mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	Merge branch 'master' into NetworkPR2
This commit is contained in:
		
						commit
						d66f88672c
					
				@ -7,7 +7,7 @@ parameters:
 | 
			
		||||
  default: "ubuntu-latest"
 | 
			
		||||
- name: DotNetSdkVersion
 | 
			
		||||
  type: string
 | 
			
		||||
  default: 3.1.100
 | 
			
		||||
  default: 5.0.100
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  - job: CompatibilityCheck
 | 
			
		||||
 | 
			
		||||
@ -35,14 +35,6 @@ jobs:
 | 
			
		||||
        customEndpoint: 'jellyfin-bot for NPM'
 | 
			
		||||
 | 
			
		||||
## Generate npm api client
 | 
			
		||||
# Unstable
 | 
			
		||||
    - task: CmdLine@2
 | 
			
		||||
      displayName: 'Build unstable typescript axios client'
 | 
			
		||||
      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 | 
			
		||||
      inputs:
 | 
			
		||||
        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
 | 
			
		||||
 | 
			
		||||
# Stable
 | 
			
		||||
    - task: CmdLine@2
 | 
			
		||||
      displayName: 'Build stable typescript axios client'
 | 
			
		||||
      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
@ -57,17 +49,6 @@ jobs:
 | 
			
		||||
        workingDir: ./apiclient/generated/typescript/axios
 | 
			
		||||
 | 
			
		||||
## Publish npm packages
 | 
			
		||||
# Unstable
 | 
			
		||||
    - task: Npm@1
 | 
			
		||||
      displayName: 'Publish unstable typescript axios client'
 | 
			
		||||
      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 | 
			
		||||
      inputs:
 | 
			
		||||
        command: publish
 | 
			
		||||
        publishRegistry: useFeed
 | 
			
		||||
        publishFeed: 'jellyfin/unstable'
 | 
			
		||||
        workingDir: ./apiclient/generated/typescript/axios
 | 
			
		||||
 | 
			
		||||
# Stable
 | 
			
		||||
    - task: Npm@1
 | 
			
		||||
      displayName: 'Publish stable typescript axios client'
 | 
			
		||||
      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
parameters:
 | 
			
		||||
  LinuxImage: 'ubuntu-latest'
 | 
			
		||||
  RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
 | 
			
		||||
  DotNetSdkVersion: 3.1.100
 | 
			
		||||
  DotNetSdkVersion: 5.0.100
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  - job: Build
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ parameters:
 | 
			
		||||
  default: "tests/**/*Tests.csproj"
 | 
			
		||||
- name: DotNetSdkVersion
 | 
			
		||||
  type: string
 | 
			
		||||
  default: 3.1.100
 | 
			
		||||
  default: 5.0.100
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  - job: Test
 | 
			
		||||
@ -94,5 +94,5 @@ jobs:
 | 
			
		||||
        displayName: 'Publish OpenAPI Artifact'
 | 
			
		||||
        condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
 | 
			
		||||
        inputs:
 | 
			
		||||
          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
 | 
			
		||||
          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
 | 
			
		||||
          artifactName: 'OpenAPI Spec'
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ variables:
 | 
			
		||||
- name: RestoreBuildProjects
 | 
			
		||||
  value: 'Jellyfin.Server/Jellyfin.Server.csproj'
 | 
			
		||||
- name: DotNetSdkVersion
 | 
			
		||||
  value: 3.1.100
 | 
			
		||||
  value: 5.0.100
 | 
			
		||||
 | 
			
		||||
pr:
 | 
			
		||||
  autoCancel: true
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@ -6,7 +6,7 @@
 | 
			
		||||
            "type": "coreclr",
 | 
			
		||||
            "request": "launch",
 | 
			
		||||
            "preLaunchTask": "build",
 | 
			
		||||
            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
 | 
			
		||||
            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "cwd": "${workspaceFolder}/Jellyfin.Server",
 | 
			
		||||
            "console": "internalConsole",
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
            "type": "coreclr",
 | 
			
		||||
            "request": "launch",
 | 
			
		||||
            "preLaunchTask": "build",
 | 
			
		||||
            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
 | 
			
		||||
            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
 | 
			
		||||
            "args": ["--nowebclient"],
 | 
			
		||||
            "cwd": "${workspaceFolder}/Jellyfin.Server",
 | 
			
		||||
            "console": "internalConsole",
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
ARG DOTNET_VERSION=3.1
 | 
			
		||||
ARG DOTNET_VERSION=5.0
 | 
			
		||||
 | 
			
		||||
FROM node:alpine as web-builder
 | 
			
		||||
ARG JELLYFIN_WEB_VERSION=master
 | 
			
		||||
@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
 | 
			
		||||
 && yarn install \
 | 
			
		||||
 && mv dist /dist
 | 
			
		||||
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
 | 
			
		||||
WORKDIR /repo
 | 
			
		||||
COPY . .
 | 
			
		||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
#####################################
 | 
			
		||||
# Requires binfm_misc registration
 | 
			
		||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 | 
			
		||||
ARG DOTNET_VERSION=3.1
 | 
			
		||||
ARG DOTNET_VERSION=5.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM node:alpine as web-builder
 | 
			
		||||
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
 | 
			
		||||
 && mv dist /dist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 | 
			
		||||
WORKDIR /repo
 | 
			
		||||
COPY . .
 | 
			
		||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
#####################################
 | 
			
		||||
# Requires binfm_misc registration
 | 
			
		||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 | 
			
		||||
ARG DOTNET_VERSION=3.1
 | 
			
		||||
ARG DOTNET_VERSION=5.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM node:alpine as web-builder
 | 
			
		||||
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
 | 
			
		||||
 && mv dist /dist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 | 
			
		||||
WORKDIR /repo
 | 
			
		||||
COPY . .
 | 
			
		||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ namespace DvdLib.Ifo
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
 | 
			
		||||
                    {
 | 
			
		||||
                        ReadVTS(ifoNumber, ifo.FullName);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl
 | 
			
		||||
        {
 | 
			
		||||
            _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool Contains(string field)
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing
 | 
			
		||||
            if (!string.IsNullOrEmpty(header))
 | 
			
		||||
            {
 | 
			
		||||
                // Starts with SECOND-
 | 
			
		||||
                header = header.Split('-').Last();
 | 
			
		||||
                header = header.Split('-')[^1];
 | 
			
		||||
 | 
			
		||||
                if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
 | 
			
		||||
                {
 | 
			
		||||
@ -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");
 | 
			
		||||
 | 
			
		||||
@ -259,7 +259,10 @@ namespace Emby.Dlna.Main
 | 
			
		||||
 | 
			
		||||
        private void RegisterServerEndpoints()
 | 
			
		||||
        {
 | 
			
		||||
            var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var udn = CreateUuid(_appHost.SystemId);
 | 
			
		||||
            var descriptorUri = "/dlna/" + udn + "/description.xml";
 | 
			
		||||
 | 
			
		||||
            var bindAddresses = NetworkManager.CreateCollection(
 | 
			
		||||
                _networkManager.GetInternalBindAddresses()
 | 
			
		||||
 | 
			
		||||
@ -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>();
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -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++)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,7 @@ namespace Emby.Notifications
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
 | 
			
		||||
        private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
 | 
			
		||||
        {
 | 
			
		||||
            var type = NotificationType.ServerRestartRequired.ToString();
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ namespace Emby.Notifications
 | 
			
		||||
            await SendNotification(notification, null).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
 | 
			
		||||
        private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
 | 
			
		||||
        {
 | 
			
		||||
            var entry = e.Argument;
 | 
			
		||||
 | 
			
		||||
@ -132,7 +132,7 @@ namespace Emby.Notifications
 | 
			
		||||
            return _config.GetConfiguration<NotificationOptions>("notifications");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
 | 
			
		||||
        private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_appHost.HasUpdateAvailable)
 | 
			
		||||
            {
 | 
			
		||||
@ -151,7 +151,7 @@ namespace Emby.Notifications
 | 
			
		||||
            await SendNotification(notification, null).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
 | 
			
		||||
        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
 | 
			
		||||
        {
 | 
			
		||||
            if (!FilterItem(e.Item))
 | 
			
		||||
            {
 | 
			
		||||
@ -197,7 +197,7 @@ namespace Emby.Notifications
 | 
			
		||||
            return item.SourceType == SourceType.Library;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void LibraryUpdateTimerCallback(object state)
 | 
			
		||||
        private async void LibraryUpdateTimerCallback(object? state)
 | 
			
		||||
        {
 | 
			
		||||
            List<BaseItem> items;
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
 | 
			
		||||
@ -139,23 +139,21 @@ namespace Emby.Server.Implementations.AppBase
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">Class to register.</typeparam>
 | 
			
		||||
        public virtual void RegisterConfiguration<T>()
 | 
			
		||||
            where T : IConfigurationFactory
 | 
			
		||||
        {
 | 
			
		||||
            if (!typeof(IConfigurationFactory).IsAssignableFrom(typeof(T)))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException("Parameter does not implement IConfigurationFactory");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IConfigurationFactory factory = (IConfigurationFactory)Activator.CreateInstance(typeof(T));
 | 
			
		||||
            IConfigurationFactory factory = Activator.CreateInstance<T>();
 | 
			
		||||
 | 
			
		||||
            if (_configurationFactories == null)
 | 
			
		||||
            {
 | 
			
		||||
                _configurationFactories = new IConfigurationFactory[] { factory };
 | 
			
		||||
                _configurationFactories = new[] { factory };
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var list = _configurationFactories.ToList<IConfigurationFactory>();
 | 
			
		||||
                list.Add(factory);
 | 
			
		||||
                _configurationFactories = list.ToArray();
 | 
			
		||||
                var oldLen = _configurationFactories.Length;
 | 
			
		||||
                var arr = new IConfigurationFactory[oldLen + 1];
 | 
			
		||||
                _configurationFactories.CopyTo(arr, 0);
 | 
			
		||||
                arr[oldLen] = factory;
 | 
			
		||||
                _configurationFactories = arr;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _configurationStores = _configurationFactories
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Model.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.AppBase
 | 
			
		||||
@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                configuration = Activator.CreateInstance(type);
 | 
			
		||||
                configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var stream = new MemoryStream(buffer?.Length ?? 0);
 | 
			
		||||
@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
 | 
			
		||||
            // If the file didn't exist before, or if something has changed, re-save
 | 
			
		||||
            if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
 | 
			
		||||
            {
 | 
			
		||||
                Directory.CreateDirectory(Path.GetDirectoryName(path));
 | 
			
		||||
                var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
 | 
			
		||||
 | 
			
		||||
                Directory.CreateDirectory(directory);
 | 
			
		||||
                // Save it after load in case we got new items
 | 
			
		||||
                using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -31,7 +30,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;
 | 
			
		||||
@ -133,7 +131,6 @@ namespace Emby.Server.Implementations
 | 
			
		||||
        private IMediaEncoder _mediaEncoder;
 | 
			
		||||
        private ISessionManager _sessionManager;
 | 
			
		||||
        private IHttpClientFactory _httpClientFactory;
 | 
			
		||||
 | 
			
		||||
        private string[] _urlPrefixes;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -510,24 +507,11 @@ namespace Emby.Server.Implementations
 | 
			
		||||
                HttpsPort = NetworkConfiguration.DefaultHttpsPort;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (Plugins != null)
 | 
			
		||||
            {
 | 
			
		||||
                var pluginBuilder = new StringBuilder();
 | 
			
		||||
 | 
			
		||||
                foreach (var plugin in Plugins)
 | 
			
		||||
                {
 | 
			
		||||
                    pluginBuilder.Append(plugin.Name)
 | 
			
		||||
                        .Append(' ')
 | 
			
		||||
                        .Append(plugin.Version)
 | 
			
		||||
                        .AppendLine();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            DiscoverTypes();
 | 
			
		||||
 | 
			
		||||
            RegisterServices();
 | 
			
		||||
 | 
			
		||||
            RegisterPluginServices();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -792,10 +776,24 @@ namespace Emby.Server.Implementations
 | 
			
		||||
 | 
			
		||||
            ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 | 
			
		||||
            _plugins = GetExports<IPlugin>()
 | 
			
		||||
                        .Select(LoadPlugin)
 | 
			
		||||
                        .Where(i => i != null)
 | 
			
		||||
                        .ToArray();
 | 
			
		||||
 | 
			
		||||
            if (Plugins != null)
 | 
			
		||||
            {
 | 
			
		||||
                var pluginBuilder = new StringBuilder();
 | 
			
		||||
 | 
			
		||||
                foreach (var plugin in Plugins)
 | 
			
		||||
                {
 | 
			
		||||
                    pluginBuilder.Append(plugin.Name)
 | 
			
		||||
                        .Append(' ')
 | 
			
		||||
                        .Append(plugin.Version)
 | 
			
		||||
                        .AppendLine();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _urlPrefixes = GetUrlPrefixes().ToArray();
 | 
			
		||||
 | 
			
		||||
            Resolve<ILibraryManager>().AddParts(
 | 
			
		||||
@ -825,21 +823,6 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IPlugin LoadPlugin(IPlugin plugin)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                plugin.RegisterServices(ServiceCollection);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return plugin;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Discovers the types.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -850,6 +833,22 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RegisterPluginServices()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
 | 
			
		||||
                    instance.RegisterServices(ServiceCollection);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var ass in assemblies)
 | 
			
		||||
@ -1005,79 +1004,59 @@ 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)
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
 | 
			
		||||
        {
 | 
			
		||||
            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
 | 
			
		||||
 | 
			
		||||
            if (compare == 0)
 | 
			
		||||
            var minimumVersion = new Version(0, 0, 0, 1);
 | 
			
		||||
            var versions = new List<LocalPlugin>();
 | 
			
		||||
            if (!Directory.Exists(path))
 | 
			
		||||
            {
 | 
			
		||||
                return a.PluginVersion.CompareTo(b.PluginVersion);
 | 
			
		||||
                // Plugin path doesn't exist, don't try to enumerate subfolders.
 | 
			
		||||
                return Enumerable.Empty<LocalPlugin>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
        {
 | 
			
		||||
            var dllList = new List<string>();
 | 
			
		||||
            var versions = new List<(Version PluginVersion, string Name, string Path)>();
 | 
			
		||||
            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
 | 
			
		||||
                    {
 | 
			
		||||
                        // No metafile, so lets see if the folder is versioned.
 | 
			
		||||
                        metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
 | 
			
		||||
                        metafile = dir.Split(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));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@ -1088,14 +1067,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;
 | 
			
		||||
                }
 | 
			
		||||
@ -1103,6 +1082,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);
 | 
			
		||||
@ -1115,7 +1095,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return dllList;
 | 
			
		||||
            return versions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -1126,21 +1106,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
 | 
			
		||||
                    foreach (var file in plugin.DllFiles)
 | 
			
		||||
                    {
 | 
			
		||||
                        plugAss = Assembly.LoadFrom(file);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (FileLoadException ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        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;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Model.Cryptography;
 | 
			
		||||
using static MediaBrowser.Common.Cryptography.Constants;
 | 
			
		||||
 | 
			
		||||
@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
 | 
			
		||||
                throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var h = HashAlgorithm.Create(hashMethod);
 | 
			
		||||
            using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
 | 
			
		||||
            if (salt.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return h.ComputeHash(bytes);
 | 
			
		||||
 | 
			
		||||
@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
 | 
			
		||||
            foreach (var part in parts)
 | 
			
		||||
            {
 | 
			
		||||
@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var list = new List<ItemImageInfo>();
 | 
			
		||||
            foreach (var part in parts)
 | 
			
		||||
            {
 | 
			
		||||
@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
 | 
			
		||||
        public ItemImageInfo ItemImageInfoFromValueString(string value)
 | 
			
		||||
        {
 | 
			
		||||
            var parts = value.Split(new[] { '*' }, StringSplitOptions.None);
 | 
			
		||||
            var parts = value.Split('*', StringSplitOptions.None);
 | 
			
		||||
 | 
			
		||||
            if (parts.Length < 3)
 | 
			
		||||
            {
 | 
			
		||||
@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
            {
 | 
			
		||||
                if (!reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                {
 | 
			
		||||
                    IEnumerable<MetadataField> GetLockedFields(string s)
 | 
			
		||||
                    {
 | 
			
		||||
                        foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
 | 
			
		||||
                        foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
 | 
			
		||||
                        {
 | 
			
		||||
                            if (Enum.TryParse(i, true, out MetadataField parsedValue))
 | 
			
		||||
                            {
 | 
			
		||||
@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
            {
 | 
			
		||||
                if (!reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
            {
 | 
			
		||||
                if (!reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                    {
 | 
			
		||||
                        IEnumerable<TrailerType> GetTrailerTypes(string s)
 | 
			
		||||
                        {
 | 
			
		||||
                            foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
 | 
			
		||||
                            foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (Enum.TryParse(i, true, out TrailerType parsedValue))
 | 
			
		||||
                                {
 | 
			
		||||
@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
            {
 | 
			
		||||
                if (!reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
 | 
			
		||||
                    item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
            {
 | 
			
		||||
                if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
 | 
			
		||||
                if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
 | 
			
		||||
                {
 | 
			
		||||
                    hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                    hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                index++;
 | 
			
		||||
@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrEmpty(item.OfficialRating))
 | 
			
		||||
                {
 | 
			
		||||
                    builder.Append("((OfficialRating is null) * 10)");
 | 
			
		||||
                    builder.Append("(OfficialRating is null * 10)");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
 | 
			
		||||
                    builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (item.ProductionYear.HasValue)
 | 
			
		||||
@ -2416,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                    builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //// genres, tags
 | 
			
		||||
                builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
 | 
			
		||||
                // genres, tags, studios, person, year?
 | 
			
		||||
                builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
 | 
			
		||||
 | 
			
		||||
                if (item is MusicArtist)
 | 
			
		||||
                {
 | 
			
		||||
                    // Match albums where the artist is AlbumArtist against other albums.
 | 
			
		||||
                    // It is assumed that similar albums => similar artists.
 | 
			
		||||
                    builder.Append(
 | 
			
		||||
                        @"+ (WITH artistValues AS (
 | 
			
		||||
	                            SELECT DISTINCT albumValues.CleanValue
 | 
			
		||||
	                            FROM ItemValues albumValues
 | 
			
		||||
	                            INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
 | 
			
		||||
	                            INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
 | 
			
		||||
                            ), similarArtist AS (
 | 
			
		||||
	                            SELECT albumValues.ItemId
 | 
			
		||||
	                            FROM ItemValues albumValues
 | 
			
		||||
	                            INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
 | 
			
		||||
	                            INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
 | 
			
		||||
                            ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                builder.Append(") as SimilarityScore");
 | 
			
		||||
 | 
			
		||||
@ -5002,26 +5020,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);
 | 
			
		||||
@ -5045,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 | 
			
		||||
 | 
			
		||||
            CheckDisposed();
 | 
			
		||||
 | 
			
		||||
            var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People";
 | 
			
		||||
            var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
 | 
			
		||||
 | 
			
		||||
            var whereClauses = GetPeopleWhereClauses(query, null);
 | 
			
		||||
 | 
			
		||||
@ -5087,19 +5112,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 +5126,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 +5140,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 +5152,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 +5438,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,
 | 
			
		||||
@ -5592,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 | 
			
		||||
                return counts;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
            var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                .ToLookup(x => x);
 | 
			
		||||
 | 
			
		||||
            foreach (var type in allTypes)
 | 
			
		||||
 | 
			
		||||
@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var containers = container.Split(new[] { ',' });
 | 
			
		||||
                var containers = container.Split(',');
 | 
			
		||||
                if (containers.Length < 2)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
@ -31,13 +31,13 @@
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
 | 
			
		||||
    <PackageReference Include="Mono.Nat" Version="3.0.0" />
 | 
			
		||||
    <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
 | 
			
		||||
    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
 | 
			
		||||
    <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" />
 | 
			
		||||
    <PackageReference Include="sharpcompress" Version="0.26.0" />
 | 
			
		||||
    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
 | 
			
		||||
    <PackageReference Include="DotNet.Glob" Version="3.1.0" />
 | 
			
		||||
@ -48,10 +48,12 @@
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>netstandard2.1</TargetFramework>
 | 
			
		||||
    <TargetFramework>net5.0</TargetFramework>
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
 | 
			
		||||
    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
 | 
			
		||||
    <NoWarn>AD0001</NoWarn>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <!-- Code Analyzers-->
 | 
			
		||||
 | 
			
		||||
@ -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.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                // Request doesn't contain a token.
 | 
			
		||||
                return authInfo;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = _authRepo.Get(new AuthenticationInfoQuery
 | 
			
		||||
            {
 | 
			
		||||
                AccessToken = token
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (result.Items.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                authInfo.IsAuthenticated = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 | 
			
		||||
 | 
			
		||||
            if (originalAuthenticationInfo != null)
 | 
			
		||||
            {
 | 
			
		||||
                var updateToken = false;
 | 
			
		||||
 | 
			
		||||
                // TODO: Remove these checks for IsNullOrWhiteSpace
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(authInfo.Client))
 | 
			
		||||
                {
 | 
			
		||||
                    AccessToken = token
 | 
			
		||||
                });
 | 
			
		||||
                    authInfo.Client = originalAuthenticationInfo.AppName;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 | 
			
		||||
 | 
			
		||||
                if (originalAuthenticationInfo != null)
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
			
		||||
                {
 | 
			
		||||
                    var updateToken = false;
 | 
			
		||||
                    authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    // TODO: Remove these checks for IsNullOrWhiteSpace
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(authInfo.Client))
 | 
			
		||||
                    {
 | 
			
		||||
                        authInfo.Client = originalAuthenticationInfo.AppName;
 | 
			
		||||
                    }
 | 
			
		||||
                // 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.DeviceId))
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(authInfo.Device))
 | 
			
		||||
                {
 | 
			
		||||
                    authInfo.Device = originalAuthenticationInfo.DeviceName;
 | 
			
		||||
                }
 | 
			
		||||
                else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    if (allowTokenInfoUpdate)
 | 
			
		||||
                    {
 | 
			
		||||
                        authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
 | 
			
		||||
                        updateToken = true;
 | 
			
		||||
                        originalAuthenticationInfo.DeviceName = authInfo.Device;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    // 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.Version))
 | 
			
		||||
                {
 | 
			
		||||
                    authInfo.Version = originalAuthenticationInfo.AppVersion;
 | 
			
		||||
                }
 | 
			
		||||
                else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    if (allowTokenInfoUpdate)
 | 
			
		||||
                    {
 | 
			
		||||
                        updateToken = true;
 | 
			
		||||
                        originalAuthenticationInfo.AppVersion = authInfo.Version;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    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 ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
 | 
			
		||||
                {
 | 
			
		||||
                    originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
 | 
			
		||||
                    updateToken = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    if (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
			
		||||
                    {
 | 
			
		||||
                        authInfo.Version = originalAuthenticationInfo.AppVersion;
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (allowTokenInfoUpdate)
 | 
			
		||||
                        {
 | 
			
		||||
                            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)
 | 
			
		||||
                    if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
 | 
			
		||||
                        originalAuthenticationInfo.UserName = authInfo.User.Username;
 | 
			
		||||
                        updateToken = true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
 | 
			
		||||
                    {
 | 
			
		||||
                        authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
 | 
			
		||||
                    authInfo.IsApiKey = true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    authInfo.IsApiKey = false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                        if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                        {
 | 
			
		||||
                            originalAuthenticationInfo.UserName = authInfo.User.Username;
 | 
			
		||||
                            updateToken = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (updateToken)
 | 
			
		||||
                    {
 | 
			
		||||
                        _authRepo.Update(originalAuthenticationInfo);
 | 
			
		||||
                    }
 | 
			
		||||
                if (updateToken)
 | 
			
		||||
                {
 | 
			
		||||
                    _authRepo.Update(originalAuthenticationInfo);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (authInfo, originalAuthenticationInfo);
 | 
			
		||||
            return authInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -238,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var parts = authorizationHeader.Split(new[] { ' ' }, 2);
 | 
			
		||||
            var parts = authorizationHeader.Split(' ', 2);
 | 
			
		||||
 | 
			
		||||
            // There should be at least to parts
 | 
			
		||||
            if (parts.Length != 2)
 | 
			
		||||
@ -262,11 +269,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
 | 
			
		||||
            foreach (var item in parts)
 | 
			
		||||
            {
 | 
			
		||||
                var param = item.Trim().Split(new[] { '=' }, 2);
 | 
			
		||||
                var param = item.Trim().Split('=', 2);
 | 
			
		||||
 | 
			
		||||
                if (param.Length == 2)
 | 
			
		||||
                {
 | 
			
		||||
                    var value = NormalizeValue(param[1].Trim(new[] { '"' }));
 | 
			
		||||
                    var value = NormalizeValue(param[1].Trim('"'));
 | 
			
		||||
                    result[param[0]] = value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Naming.Audio;
 | 
			
		||||
@ -2440,6 +2441,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)
 | 
			
		||||
        {
 | 
			
		||||
@ -2690,7 +2706,7 @@ namespace Emby.Server.Implementations.Library
 | 
			
		||||
 | 
			
		||||
            var videos = videoListResolver.Resolve(fileSystemChildren);
 | 
			
		||||
 | 
			
		||||
            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
 | 
			
		||||
            if (currentVideo != null)
 | 
			
		||||
            {
 | 
			
		||||
@ -2892,7 +2908,7 @@ namespace Emby.Server.Implementations.Library
 | 
			
		||||
 | 
			
		||||
                    return item.GetImageInfo(image.Type, imageIndex);
 | 
			
		||||
                }
 | 
			
		||||
                catch (HttpException ex)
 | 
			
		||||
                catch (HttpRequestException ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (ex.StatusCode.HasValue
 | 
			
		||||
                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
 | 
			
		||||
 | 
			
		||||
@ -849,7 +849,7 @@ namespace Emby.Server.Implementations.Library
 | 
			
		||||
                throw new ArgumentException("Key can't be empty.", nameof(key));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
 | 
			
		||||
            var keys = key.Split(LiveStreamIdDelimeter, 2);
 | 
			
		||||
 | 
			
		||||
            var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -201,7 +201,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var firstMedia = resolvedItem.Files.First();
 | 
			
		||||
                var firstMedia = resolvedItem.Files[0];
 | 
			
		||||
 | 
			
		||||
                var libraryItem = new T
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -587,7 +591,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 | 
			
		||||
                savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                if (ex.StatusCode.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
@ -617,7 +621,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 | 
			
		||||
            {
 | 
			
		||||
                return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                _tokens.Clear();
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
@ -705,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 | 
			
		||||
 | 
			
		||||
                return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                // Apparently we're supposed to swallow this
 | 
			
		||||
                if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
 | 
			
		||||
 | 
			
		||||
@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null)
 | 
			
		||||
        public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
 | 
			
		||||
        {
 | 
			
		||||
            var programTuples = new List<Tuple<BaseItemDto, string, string>>();
 | 
			
		||||
            var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
 | 
			
		||||
@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        public Task ResetTuner(string id, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var parts = id.Split(new[] { '_' }, 2);
 | 
			
		||||
            var parts = id.Split('_', 2);
 | 
			
		||||
 | 
			
		||||
            var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
			
		||||
 | 
			
		||||
                return discoverResponse;
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
 | 
			
		||||
                {
 | 
			
		||||
@ -663,7 +663,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
			
		||||
                var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
                info.DeviceId = modelInfo.DeviceID;
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -153,6 +153,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 () =>
 | 
			
		||||
 | 
			
		||||
@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(currentFile))
 | 
			
		||||
            {
 | 
			
		||||
                return (files.Last(), true);
 | 
			
		||||
                return (files[^1], true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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:";
 | 
			
		||||
@ -153,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
 | 
			
		||||
        private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
 | 
			
		||||
        {
 | 
			
		||||
            var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 | 
			
		||||
 | 
			
		||||
            string numberString = null;
 | 
			
		||||
@ -263,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
 | 
			
		||||
        private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
 | 
			
		||||
        {
 | 
			
		||||
            var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
 | 
			
		||||
            var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
            var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null;
 | 
			
		||||
 | 
			
		||||
            // Check for channel number with the format from SatIp
 | 
			
		||||
            // #EXTINF:0,84. VOX Schweiz
 | 
			
		||||
 | 
			
		||||
@ -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 () =>
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -113,5 +113,7 @@
 | 
			
		||||
    "TasksChannelsCategory": "Internet Channels",
 | 
			
		||||
    "TasksApplicationCategory": "Application",
 | 
			
		||||
    "TasksLibraryCategory": "Library",
 | 
			
		||||
    "TasksMaintenanceCategory": "Maintenance"
 | 
			
		||||
    "TasksMaintenanceCategory": "Maintenance",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
 | 
			
		||||
    "TaskCleanActivityLog": "Clean Activity Log"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,11 +9,13 @@
 | 
			
		||||
    "Channels": "Channels",
 | 
			
		||||
    "ChapterNameValue": "Chapter {0}",
 | 
			
		||||
    "Collections": "Collections",
 | 
			
		||||
    "Default": "Default",
 | 
			
		||||
    "DeviceOfflineWithName": "{0} has disconnected",
 | 
			
		||||
    "DeviceOnlineWithName": "{0} is connected",
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
 | 
			
		||||
    "Favorites": "Favorites",
 | 
			
		||||
    "Folders": "Folders",
 | 
			
		||||
    "Forced": "Forced",
 | 
			
		||||
    "Genres": "Genres",
 | 
			
		||||
    "HeaderAlbumArtists": "Album Artists",
 | 
			
		||||
    "HeaderContinueWatching": "Continue Watching",
 | 
			
		||||
@ -77,6 +79,7 @@
 | 
			
		||||
    "Sync": "Sync",
 | 
			
		||||
    "System": "System",
 | 
			
		||||
    "TvShows": "TV Shows",
 | 
			
		||||
    "Undefined": "Undefined",
 | 
			
		||||
    "User": "User",
 | 
			
		||||
    "UserCreatedWithName": "User {0} has been created",
 | 
			
		||||
    "UserDeletedWithName": "User {0} has been deleted",
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "VersionNumber": "Bersyon {0}",
 | 
			
		||||
    "ValueSpecialEpisodeName": "Espesyal - {0}",
 | 
			
		||||
    "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library",
 | 
			
		||||
    "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
 | 
			
		||||
    "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
 | 
			
		||||
    "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
 | 
			
		||||
    "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
 | 
			
		||||
@ -61,8 +61,8 @@
 | 
			
		||||
    "Latest": "Pinakabago",
 | 
			
		||||
    "LabelRunningTimeValue": "Oras: {0}",
 | 
			
		||||
    "LabelIpAddressValue": "Ang IP Address ay {0}",
 | 
			
		||||
    "ItemRemovedWithName": "Naitanggal ang {0} sa library",
 | 
			
		||||
    "ItemAddedWithName": "Naidagdag ang {0} sa library",
 | 
			
		||||
    "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
 | 
			
		||||
    "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
 | 
			
		||||
    "Inherit": "Manahin",
 | 
			
		||||
    "HeaderRecordingGroups": "Pagtatalang Grupo",
 | 
			
		||||
    "HeaderNextUp": "Susunod",
 | 
			
		||||
@ -90,12 +90,29 @@
 | 
			
		||||
    "Application": "Aplikasyon",
 | 
			
		||||
    "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
 | 
			
		||||
    "Albums": "Albums",
 | 
			
		||||
    "TaskRefreshLibrary": "Suriin ang nasa librerya",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata",
 | 
			
		||||
    "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
 | 
			
		||||
    "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
 | 
			
		||||
    "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
 | 
			
		||||
    "TasksChannelsCategory": "Palabas sa internet",
 | 
			
		||||
    "TasksLibraryCategory": "Librerya",
 | 
			
		||||
    "TasksMaintenanceCategory": "Pagpapanatili",
 | 
			
		||||
    "HomeVideos": "Sariling pelikula"
 | 
			
		||||
    "HomeVideos": "Sariling pelikula",
 | 
			
		||||
    "TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
 | 
			
		||||
    "TaskRefreshPeople": "I-refresh ang Tauhan",
 | 
			
		||||
    "TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "I-download and nawawalang subtitles",
 | 
			
		||||
    "TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
 | 
			
		||||
    "TaskRefreshChannels": "I-refresh ang Channels",
 | 
			
		||||
    "TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
 | 
			
		||||
    "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
 | 
			
		||||
    "TaskUpdatePlugins": "I-update ang Plugins",
 | 
			
		||||
    "TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
 | 
			
		||||
    "TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
 | 
			
		||||
    "TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
 | 
			
		||||
    "TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
 | 
			
		||||
    "TaskCleanCache": "Linisin and Direktoryo ng Cache",
 | 
			
		||||
    "TasksApplicationCategory": "Application",
 | 
			
		||||
    "TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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é"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -113,5 +113,7 @@
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
 | 
			
		||||
    "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
 | 
			
		||||
    "TaskRefreshChannels": "Csatornák frissítése",
 | 
			
		||||
    "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat."
 | 
			
		||||
    "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
 | 
			
		||||
    "TaskCleanActivityLog": "Tevékenységnapló törlése"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@
 | 
			
		||||
    "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
 | 
			
		||||
    "TaskRefreshLibrary": "メディアライブラリのスキャン",
 | 
			
		||||
    "TaskCleanCacheDescription": "不要なキャッシュを消去します。",
 | 
			
		||||
    "TaskCleanCache": "キャッシュの掃除",
 | 
			
		||||
    "TaskCleanCache": "キャッシュを消去",
 | 
			
		||||
    "TasksChannelsCategory": "ネットチャンネル",
 | 
			
		||||
    "TasksApplicationCategory": "アプリケーション",
 | 
			
		||||
    "TasksLibraryCategory": "ライブラリ",
 | 
			
		||||
@ -112,5 +112,7 @@
 | 
			
		||||
    "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
 | 
			
		||||
    "TaskRefreshChapterImages": "チャプター画像を抽出する",
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
 | 
			
		||||
    "TaskCleanActivityLog": "アクティビティの履歴を消去"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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": "활동내역청소"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -113,5 +113,7 @@
 | 
			
		||||
    "TasksChannelsCategory": "Internet Kanalen",
 | 
			
		||||
    "TasksApplicationCategory": "Applicatie",
 | 
			
		||||
    "TasksLibraryCategory": "Bibliotheek",
 | 
			
		||||
    "TasksMaintenanceCategory": "Onderhoud"
 | 
			
		||||
    "TasksMaintenanceCategory": "Onderhoud",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
 | 
			
		||||
    "TaskCleanActivityLog": "Leeg activiteiten logboek"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -112,5 +112,7 @@
 | 
			
		||||
    "TasksChannelsCategory": "Canale de pe Internet",
 | 
			
		||||
    "TasksApplicationCategory": "Aplicație",
 | 
			
		||||
    "TasksLibraryCategory": "Librărie",
 | 
			
		||||
    "TasksMaintenanceCategory": "Mentenanță"
 | 
			
		||||
    "TasksMaintenanceCategory": "Mentenanță",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
 | 
			
		||||
    "TaskCleanActivityLog": "Curăță Jurnalul de Activitate"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -113,5 +113,7 @@
 | 
			
		||||
    "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
 | 
			
		||||
    "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
 | 
			
		||||
    "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
 | 
			
		||||
    "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
 | 
			
		||||
    "TaskCleanActivityLog": "Очистить журнал активности"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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.",
 | 
			
		||||
 | 
			
		||||
@ -112,5 +112,7 @@
 | 
			
		||||
    "TasksChannelsCategory": "Интернет канали",
 | 
			
		||||
    "TasksApplicationCategory": "Апликација",
 | 
			
		||||
    "TasksLibraryCategory": "Библиотека",
 | 
			
		||||
    "TasksMaintenanceCategory": "Одржавање"
 | 
			
		||||
    "TasksMaintenanceCategory": "Одржавање",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
 | 
			
		||||
    "TaskCleanActivityLog": "Очисти историју активности"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -112,5 +112,7 @@
 | 
			
		||||
    "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
 | 
			
		||||
    "HomeVideos": "முகப்பு வீடியோக்கள்",
 | 
			
		||||
    "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
 | 
			
		||||
    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 | 
			
		||||
    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.",
 | 
			
		||||
    "TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -113,5 +113,7 @@
 | 
			
		||||
    "TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
 | 
			
		||||
    "TaskCleanCache": "清理缓存目录",
 | 
			
		||||
    "TasksApplicationCategory": "应用程序",
 | 
			
		||||
    "TasksMaintenanceCategory": "维护"
 | 
			
		||||
    "TasksMaintenanceCategory": "维护",
 | 
			
		||||
    "TaskCleanActivityLog": "清理程序日志",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -112,5 +112,7 @@
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
 | 
			
		||||
    "TasksChannelsCategory": "網路頻道",
 | 
			
		||||
    "TasksApplicationCategory": "應用程式",
 | 
			
		||||
    "TasksMaintenanceCategory": "維修"
 | 
			
		||||
    "TasksMaintenanceCategory": "維護",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
 | 
			
		||||
    "TaskCleanActivityLog": "清除活動紀錄"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -653,7 +653,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        _logger.LogInformation(Name + ": Waiting on Task");
 | 
			
		||||
                        var exited = Task.WaitAll(new[] { task }, 2000);
 | 
			
		||||
                        var exited = task.Wait(2000);
 | 
			
		||||
 | 
			
		||||
                        if (exited)
 | 
			
		||||
                        {
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    previouslyFailedImages = File.ReadAllText(failHistoryPath)
 | 
			
		||||
                        .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                        .Split('|', StringSplitOptions.RemoveEmptyEntries)
 | 
			
		||||
                        .ToList();
 | 
			
		||||
                }
 | 
			
		||||
                catch (IOException)
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Common.Updates;
 | 
			
		||||
@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 | 
			
		||||
                        throw;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (HttpException ex)
 | 
			
		||||
                catch (HttpRequestException ex)
 | 
			
		||||
                {
 | 
			
		||||
                    _logger.LogError(ex, "Error downloading {0}", package.Name);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ using System.Linq;
 | 
			
		||||
using System.Net.WebSockets;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
using MediaBrowser.Model.Net;
 | 
			
		||||
@ -55,9 +56,9 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
            connection.Closed += OnConnectionClosed;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnConnectionClosed(object sender, EventArgs e)
 | 
			
		||||
        private void OnConnectionClosed(object? sender, EventArgs e)
 | 
			
		||||
        {
 | 
			
		||||
            var connection = (IWebSocketConnection)sender;
 | 
			
		||||
            var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
 | 
			
		||||
            _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
 | 
			
		||||
            _sockets.Remove(connection);
 | 
			
		||||
            connection.Closed -= OnConnectionClosed;
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ 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;
 | 
			
		||||
@ -25,7 +25,6 @@ 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
 | 
			
		||||
{
 | 
			
		||||
@ -49,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;
 | 
			
		||||
 | 
			
		||||
@ -67,7 +66,7 @@ namespace Emby.Server.Implementations.Updates
 | 
			
		||||
 | 
			
		||||
        public InstallationManager(
 | 
			
		||||
            ILogger<InstallationManager> logger,
 | 
			
		||||
            IApplicationHost appHost,
 | 
			
		||||
            IServerApplicationHost appHost,
 | 
			
		||||
            IApplicationPaths appPaths,
 | 
			
		||||
            IEventManager eventManager,
 | 
			
		||||
            IHttpClientFactory httpClientFactory,
 | 
			
		||||
@ -117,11 +116,6 @@ namespace Emby.Server.Implementations.Updates
 | 
			
		||||
                _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
 | 
			
		||||
                return Array.Empty<PackageInfo>();
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpException ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
 | 
			
		||||
                return Array.Empty<PackageInfo>();
 | 
			
		||||
            }
 | 
			
		||||
            catch (HttpRequestException ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
 | 
			
		||||
@ -217,7 +211,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);
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -34,5 +34,10 @@
 | 
			
		||||
        /// Token.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string Token = "Jellyfin-Token";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Is Api Key.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string IsApiKey = "Jellyfin-IsApiKey";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,135 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
using MediaBrowser.Controller.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Entities.Audio;
 | 
			
		||||
using MediaBrowser.Controller.Library;
 | 
			
		||||
using MediaBrowser.Model.Dto;
 | 
			
		||||
using MediaBrowser.Model.Querying;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Api.Controllers
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The albums controller.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Route("")]
 | 
			
		||||
    public class AlbumsController : BaseJellyfinApiController
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IUserManager _userManager;
 | 
			
		||||
        private readonly ILibraryManager _libraryManager;
 | 
			
		||||
        private readonly IDtoService _dtoService;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="AlbumsController"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 | 
			
		||||
        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 | 
			
		||||
        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 | 
			
		||||
        public AlbumsController(
 | 
			
		||||
            IUserManager userManager,
 | 
			
		||||
            ILibraryManager libraryManager,
 | 
			
		||||
            IDtoService dtoService)
 | 
			
		||||
        {
 | 
			
		||||
            _userManager = userManager;
 | 
			
		||||
            _libraryManager = libraryManager;
 | 
			
		||||
            _dtoService = dtoService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds albums similar to a given album.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="albumId">The album id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <response code="200">Similar albums returned.</response>
 | 
			
		||||
        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
 | 
			
		||||
        [HttpGet("Albums/{albumId}/Similar")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
 | 
			
		||||
            [FromRoute, Required] string albumId,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] string? excludeArtistIds,
 | 
			
		||||
            [FromQuery] int? limit)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions().AddClientFields(Request);
 | 
			
		||||
 | 
			
		||||
            return SimilarItemsHelper.GetSimilarItemsResult(
 | 
			
		||||
                dtoOptions,
 | 
			
		||||
                _userManager,
 | 
			
		||||
                _libraryManager,
 | 
			
		||||
                _dtoService,
 | 
			
		||||
                userId,
 | 
			
		||||
                albumId,
 | 
			
		||||
                excludeArtistIds,
 | 
			
		||||
                limit,
 | 
			
		||||
                new[] { typeof(MusicAlbum) },
 | 
			
		||||
                GetAlbumSimilarityScore);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds artists similar to a given artist.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="artistId">The artist id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <response code="200">Similar artists returned.</response>
 | 
			
		||||
        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
 | 
			
		||||
        [HttpGet("Artists/{artistId}/Similar")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
 | 
			
		||||
            [FromRoute, Required] string artistId,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] string? excludeArtistIds,
 | 
			
		||||
            [FromQuery] int? limit)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions().AddClientFields(Request);
 | 
			
		||||
 | 
			
		||||
            return SimilarItemsHelper.GetSimilarItemsResult(
 | 
			
		||||
                dtoOptions,
 | 
			
		||||
                _userManager,
 | 
			
		||||
                _libraryManager,
 | 
			
		||||
                _dtoService,
 | 
			
		||||
                userId,
 | 
			
		||||
                artistId,
 | 
			
		||||
                excludeArtistIds,
 | 
			
		||||
                limit,
 | 
			
		||||
                new[] { typeof(MusicArtist) },
 | 
			
		||||
                SimilarItemsHelper.GetSimiliarityScore);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a similairty score of two albums.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="item1">The first item.</param>
 | 
			
		||||
        /// <param name="item1People">The item1 people.</param>
 | 
			
		||||
        /// <param name="allPeople">All people.</param>
 | 
			
		||||
        /// <param name="item2">The second item.</param>
 | 
			
		||||
        /// <returns>System.Int32.</returns>
 | 
			
		||||
        private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
 | 
			
		||||
        {
 | 
			
		||||
            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
 | 
			
		||||
 | 
			
		||||
            var album1 = (MusicAlbum)item1;
 | 
			
		||||
            var album2 = (MusicAlbum)item2;
 | 
			
		||||
 | 
			
		||||
            var artists1 = album1
 | 
			
		||||
                .GetAllArtists()
 | 
			
		||||
                .DistinctNames()
 | 
			
		||||
                .ToList();
 | 
			
		||||
 | 
			
		||||
            var artists2 = new HashSet<string>(
 | 
			
		||||
                album2.GetAllArtists().DistinctNames(),
 | 
			
		||||
                StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            return points + artists1.Where(artists2.Contains).Sum(i => 5);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="searchTerm">Optional. 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="fields">Optional. Specify additional fields of information to return in the output.</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>
 | 
			
		||||
@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? searchTerm,
 | 
			
		||||
            [FromQuery] string? parentId,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] string? excludeItemTypes,
 | 
			
		||||
            [FromQuery] string? includeItemTypes,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
 | 
			
		||||
@ -101,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,
 | 
			
		||||
@ -114,8 +114,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] bool? enableImages = true,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
@ -262,7 +261,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="searchTerm">Optional. 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="fields">Optional. Specify additional fields of information to return in the output.</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>
 | 
			
		||||
@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? searchTerm,
 | 
			
		||||
            [FromQuery] string? parentId,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] string? excludeItemTypes,
 | 
			
		||||
            [FromQuery] string? includeItemTypes,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
 | 
			
		||||
@ -310,7 +309,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,
 | 
			
		||||
@ -323,8 +322,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] bool? enableImages = true,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
 | 
			
		||||
        /// <param name="filters">Optional. Specify additional filters to apply.</param>
 | 
			
		||||
        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <response code="200">Channel items returned.</response>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
        /// A <see cref="Task"/> representing the request to get the channel items.
 | 
			
		||||
@ -124,7 +124,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] string? sortOrder,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
 | 
			
		||||
            [FromQuery] string? sortBy,
 | 
			
		||||
            [FromQuery] string? fields)
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
 | 
			
		||||
        {
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
@ -137,8 +137,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                ChannelIds = new[] { channelId },
 | 
			
		||||
                ParentId = folderId ?? Guid.Empty,
 | 
			
		||||
                OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
 | 
			
		||||
                DtoOptions = new DtoOptions()
 | 
			
		||||
                    .AddItemFields(fields)
 | 
			
		||||
                DtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            foreach (var filter in filters)
 | 
			
		||||
@ -185,7 +184,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <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="filters">Optional. Specify additional filters to apply.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
 | 
			
		||||
        /// <response code="200">Latest channel items returned.</response>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
@ -198,7 +197,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] int? startIndex,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] string? channelIds)
 | 
			
		||||
        {
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
@ -214,8 +213,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                    .Where(i => !string.IsNullOrWhiteSpace(i))
 | 
			
		||||
                    .Select(i => new Guid(i))
 | 
			
		||||
                    .ToArray(),
 | 
			
		||||
                DtoOptions = new DtoOptions()
 | 
			
		||||
                    .AddItemFields(fields)
 | 
			
		||||
                DtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            foreach (var filter in filters)
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.Models.PlaybackDtos;
 | 
			
		||||
using Jellyfin.Api.Models.StreamingDtos;
 | 
			
		||||
using MediaBrowser.Common.Configuration;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using MediaBrowser.Controller.Devices;
 | 
			
		||||
using MediaBrowser.Controller.Dlna;
 | 
			
		||||
@ -295,6 +296,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 +353,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 +406,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 +626,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 +681,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 +734,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 +964,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 +1023,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 +1076,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
 | 
			
		||||
                AudioSampleRate = audioSampleRate,
 | 
			
		||||
                MaxAudioChannels = maxAudioChannels,
 | 
			
		||||
                AudioBitRate = audioBitRate,
 | 
			
		||||
                AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 | 
			
		||||
                MaxAudioBitDepth = maxAudioBitDepth,
 | 
			
		||||
                AudioChannels = audioChannels,
 | 
			
		||||
                Profile = profile,
 | 
			
		||||
@ -1341,7 +1348,9 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
 | 
			
		||||
            var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
 | 
			
		||||
 | 
			
		||||
            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
 | 
			
		||||
            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
 | 
			
		||||
 | 
			
		||||
            var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
 | 
			
		||||
 | 
			
		||||
            var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
 | 
			
		||||
            if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
@ -1559,8 +1568,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
 | 
			
		||||
        private string GetSegmentPath(StreamState state, string playlist, int index)
 | 
			
		||||
        {
 | 
			
		||||
            var folder = Path.GetDirectoryName(playlist);
 | 
			
		||||
 | 
			
		||||
            var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
 | 
			
		||||
            var filename = Path.GetFileNameWithoutExtension(playlist);
 | 
			
		||||
 | 
			
		||||
            return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Models.EnvironmentDtos;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Model.IO;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
@ -103,6 +104,11 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
 | 
			
		||||
                if (validatePathDto.ValidateWritable)
 | 
			
		||||
                {
 | 
			
		||||
                    if (validatePathDto.Path == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        throw new ResourceNotFoundException(nameof(validatePathDto.Path));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
@ -78,8 +78,8 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            var query = new InternalItemsQuery
 | 
			
		||||
            {
 | 
			
		||||
                User = user,
 | 
			
		||||
                MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                Recursive = true,
 | 
			
		||||
                EnableTotalRecordCount = false,
 | 
			
		||||
                DtoOptions = new DtoOptions
 | 
			
		||||
@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            var genreQuery = new InternalItemsQuery(user)
 | 
			
		||||
            {
 | 
			
		||||
                IncludeItemTypes =
 | 
			
		||||
                    (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                    (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
 | 
			
		||||
                DtoOptions = new DtoOptions
 | 
			
		||||
                {
 | 
			
		||||
                    Fields = Array.Empty<ItemFields>(),
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
@ -49,30 +48,16 @@ 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>
 | 
			
		||||
        /// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
 | 
			
		||||
@ -84,30 +69,16 @@ 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,
 | 
			
		||||
            [FromQuery] string? parentId,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] string? excludeItemTypes,
 | 
			
		||||
            [FromQuery] string? includeItemTypes,
 | 
			
		||||
            [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] string? personTypes,
 | 
			
		||||
            [FromQuery] string? studios,
 | 
			
		||||
            [FromQuery] string? studioIds,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] string? nameStartsWithOrGreater,
 | 
			
		||||
            [FromQuery] string? nameStartsWith,
 | 
			
		||||
@ -115,45 +86,24 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] bool? enableImages = true,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = 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
 | 
			
		||||
@ -171,87 +121,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);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                result = _libraryManager.GetGenres(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return new QueryResult<BaseItemDto>
 | 
			
		||||
            {
 | 
			
		||||
                Items = dtos.ToArray(),
 | 
			
		||||
                TotalRecordCount = result.TotalRecordCount
 | 
			
		||||
            };
 | 
			
		||||
            var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
 | 
			
		||||
            return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -293,7 +176,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            return _dtoService.GetBaseItemDto(item, dtoOptions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
 | 
			
		||||
        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
 | 
			
		||||
            where T : BaseItem, new()
 | 
			
		||||
        {
 | 
			
		||||
            var result = libraryManager.GetItemList(new InternalItemsQuery
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using MediaBrowser.Common.Configuration;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using MediaBrowser.Controller.MediaEncoding;
 | 
			
		||||
using MediaBrowser.Model.IO;
 | 
			
		||||
@ -134,7 +135,8 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
 | 
			
		||||
                .FirstOrDefault(i =>
 | 
			
		||||
                    string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                    && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
 | 
			
		||||
                    && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                ?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid.");
 | 
			
		||||
 | 
			
		||||
            return GetFileResult(file, playlistPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="theme">Theme to search.</param>
 | 
			
		||||
        /// <param name="name">File name to search for.</param>
 | 
			
		||||
        /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
 | 
			
		||||
        private ActionResult GetImageFile(string basePath, string? theme, string? name)
 | 
			
		||||
        private ActionResult GetImageFile(string basePath, string theme, string? name)
 | 
			
		||||
        {
 | 
			
		||||
            var themeFolder = Path.Combine(basePath, theme);
 | 
			
		||||
            if (Directory.Exists(themeFolder))
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Mime;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Api.Attributes;
 | 
			
		||||
@ -1268,7 +1269,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                Response.Headers.Add(key, value);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Response.ContentType = imageContentType;
 | 
			
		||||
            Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
 | 
			
		||||
            Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
 | 
			
		||||
            Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -55,7 +56,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -68,18 +69,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
 | 
			
		||||
@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -105,18 +105,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
 | 
			
		||||
@ -129,7 +128,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -142,18 +141,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
 | 
			
		||||
@ -166,7 +164,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="name">The genre name.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -179,17 +177,16 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] string name,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
 | 
			
		||||
@ -202,7 +199,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -215,18 +212,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
 | 
			
		||||
@ -239,7 +235,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -252,18 +248,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
 | 
			
		||||
@ -276,7 +271,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="id">The item id.</param>
 | 
			
		||||
        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
 | 
			
		||||
        /// <param name="limit">Optional. The maximum number of records to return.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
 | 
			
		||||
        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 | 
			
		||||
@ -289,18 +284,17 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromRoute, Required] Guid id,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 | 
			
		||||
            var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
 | 
			
		||||
@ -316,9 +310,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);
 | 
			
		||||
 | 
			
		||||
@ -334,10 +334,16 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
 | 
			
		||||
        {
 | 
			
		||||
            using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
            if (result.Content.Headers.ContentType?.MediaType == null)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
 | 
			
		||||
            var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 | 
			
		||||
 | 
			
		||||
            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
 | 
			
		||||
            var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
 | 
			
		||||
            Directory.CreateDirectory(directory);
 | 
			
		||||
            using (var stream = result.Content)
 | 
			
		||||
            {
 | 
			
		||||
                await using var fileStream = new FileStream(
 | 
			
		||||
@ -351,7 +357,9 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                await stream.CopyToAsync(fileStream).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
 | 
			
		||||
            var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
 | 
			
		||||
 | 
			
		||||
            Directory.CreateDirectory(pointerCacheDirectory);
 | 
			
		||||
            await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -180,13 +180,13 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] string? searchTerm,
 | 
			
		||||
            [FromQuery] string? sortOrder,
 | 
			
		||||
            [FromQuery] string? parentId,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] string? excludeItemTypes,
 | 
			
		||||
            [FromQuery] string? includeItemTypes,
 | 
			
		||||
            [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,
 | 
			
		||||
@ -195,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,
 | 
			
		||||
@ -234,8 +234,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
@ -533,11 +532,11 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? searchTerm,
 | 
			
		||||
            [FromQuery] string? parentId,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [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,
 | 
			
		||||
@ -545,8 +544,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId);
 | 
			
		||||
            var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,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.LibraryDtos;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Common.Progress;
 | 
			
		||||
@ -455,7 +456,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                : null;
 | 
			
		||||
 | 
			
		||||
            var dtoOptions = new DtoOptions().AddClientFields(Request);
 | 
			
		||||
            BaseItem parent = item.GetParent();
 | 
			
		||||
            BaseItem? parent = item.GetParent();
 | 
			
		||||
 | 
			
		||||
            while (parent != null)
 | 
			
		||||
            {
 | 
			
		||||
@ -466,7 +467,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
 | 
			
		||||
                baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
 | 
			
		||||
 | 
			
		||||
                parent = parent.GetParent();
 | 
			
		||||
                parent = parent?.GetParent();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return baseItemDtos;
 | 
			
		||||
@ -680,12 +681,12 @@ 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, TrailerUrls.</param>
 | 
			
		||||
        /// <response code="200">Similar items returned.</response>
 | 
			
		||||
        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
 | 
			
		||||
        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
 | 
			
		||||
        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
 | 
			
		||||
        [HttpGet("Items/{itemId}/Similar")]
 | 
			
		||||
        [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
 | 
			
		||||
        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
 | 
			
		||||
        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
 | 
			
		||||
        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
 | 
			
		||||
        [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
 | 
			
		||||
        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
 | 
			
		||||
        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
 | 
			
		||||
        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
 | 
			
		||||
        [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
 | 
			
		||||
@ -693,7 +694,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] string? excludeArtistIds,
 | 
			
		||||
            [FromQuery] Guid? userId,
 | 
			
		||||
            [FromQuery] int? limit,
 | 
			
		||||
            [FromQuery] string? fields)
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
 | 
			
		||||
        {
 | 
			
		||||
            var item = itemId.Equals(Guid.Empty)
 | 
			
		||||
                ? (!userId.Equals(Guid.Empty)
 | 
			
		||||
@ -701,33 +702,71 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                    : _libraryManager.RootFolder)
 | 
			
		||||
                : _libraryManager.GetItemById(itemId);
 | 
			
		||||
 | 
			
		||||
            var program = item as IHasProgramAttributes;
 | 
			
		||||
            var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
 | 
			
		||||
            if (program != null && program.IsSeries)
 | 
			
		||||
            {
 | 
			
		||||
                return GetSimilarItemsResult(
 | 
			
		||||
                    item,
 | 
			
		||||
                    excludeArtistIds,
 | 
			
		||||
                    userId,
 | 
			
		||||
                    limit,
 | 
			
		||||
                    fields,
 | 
			
		||||
                    new[] { nameof(Series) },
 | 
			
		||||
                    false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
 | 
			
		||||
            if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
 | 
			
		||||
            {
 | 
			
		||||
                return new QueryResult<BaseItemDto>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetSimilarItemsResult(
 | 
			
		||||
                item,
 | 
			
		||||
                excludeArtistIds,
 | 
			
		||||
                userId,
 | 
			
		||||
                limit,
 | 
			
		||||
                fields,
 | 
			
		||||
                new[] { item.GetType().Name },
 | 
			
		||||
                isMovie);
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request);
 | 
			
		||||
 | 
			
		||||
            var program = item as IHasProgramAttributes;
 | 
			
		||||
            bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
 | 
			
		||||
            bool? isSeries = item is Series || (program != null && program.IsSeries);
 | 
			
		||||
 | 
			
		||||
            var includeItemTypes = new List<string>();
 | 
			
		||||
            if (isMovie.Value)
 | 
			
		||||
            {
 | 
			
		||||
                includeItemTypes.Add(nameof(Movie));
 | 
			
		||||
                if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
 | 
			
		||||
                {
 | 
			
		||||
                    includeItemTypes.Add(nameof(Trailer));
 | 
			
		||||
                    includeItemTypes.Add(nameof(LiveTvProgram));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (isSeries.Value)
 | 
			
		||||
            {
 | 
			
		||||
                includeItemTypes.Add(nameof(Series));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // For non series and movie types these columns are typically null
 | 
			
		||||
                isSeries = null;
 | 
			
		||||
                isMovie = null;
 | 
			
		||||
                includeItemTypes.Add(item.GetType().Name);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var query = new InternalItemsQuery(user)
 | 
			
		||||
            {
 | 
			
		||||
                Limit = limit,
 | 
			
		||||
                IncludeItemTypes = includeItemTypes.ToArray(),
 | 
			
		||||
                IsMovie = isMovie,
 | 
			
		||||
                IsSeries = isSeries,
 | 
			
		||||
                SimilarTo = item,
 | 
			
		||||
                DtoOptions = dtoOptions,
 | 
			
		||||
                EnableTotalRecordCount = !isMovie ?? true,
 | 
			
		||||
                EnableGroupByMetadataKey = isMovie ?? false,
 | 
			
		||||
                MinSimilarityScore = 2 // A remnant from album/artist scoring
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // ExcludeArtistIds
 | 
			
		||||
            if (!string.IsNullOrEmpty(excludeArtistIds))
 | 
			
		||||
            {
 | 
			
		||||
                query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
 | 
			
		||||
 | 
			
		||||
            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
 | 
			
		||||
 | 
			
		||||
            return new QueryResult<BaseItemDto>
 | 
			
		||||
            {
 | 
			
		||||
                Items = returnList,
 | 
			
		||||
                TotalRecordCount = itemsResult.Count
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -854,7 +893,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            return _libraryManager.GetItemsResult(query).TotalRecordCount;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private BaseItem TranslateParentItem(BaseItem item, User user)
 | 
			
		||||
        private BaseItem? TranslateParentItem(BaseItem item, User user)
 | 
			
		||||
        {
 | 
			
		||||
            return item.GetParent() is AggregateFolder
 | 
			
		||||
                ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
 | 
			
		||||
@ -880,75 +919,6 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private QueryResult<BaseItemDto> GetSimilarItemsResult(
 | 
			
		||||
            BaseItem item,
 | 
			
		||||
            string? excludeArtistIds,
 | 
			
		||||
            Guid? userId,
 | 
			
		||||
            int? limit,
 | 
			
		||||
            string? fields,
 | 
			
		||||
            string[] includeItemTypes,
 | 
			
		||||
            bool isMovie)
 | 
			
		||||
        {
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
                ? _userManager.GetUserById(userId.Value)
 | 
			
		||||
                : null;
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
                .AddClientFields(Request);
 | 
			
		||||
 | 
			
		||||
            var query = new InternalItemsQuery(user)
 | 
			
		||||
            {
 | 
			
		||||
                Limit = limit,
 | 
			
		||||
                IncludeItemTypes = includeItemTypes,
 | 
			
		||||
                IsMovie = isMovie,
 | 
			
		||||
                SimilarTo = item,
 | 
			
		||||
                DtoOptions = dtoOptions,
 | 
			
		||||
                EnableTotalRecordCount = !isMovie,
 | 
			
		||||
                EnableGroupByMetadataKey = isMovie
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // ExcludeArtistIds
 | 
			
		||||
            if (!string.IsNullOrEmpty(excludeArtistIds))
 | 
			
		||||
            {
 | 
			
		||||
                query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            List<BaseItem> itemsResult;
 | 
			
		||||
 | 
			
		||||
            if (isMovie)
 | 
			
		||||
            {
 | 
			
		||||
                var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
 | 
			
		||||
                if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
 | 
			
		||||
                {
 | 
			
		||||
                    itemTypes.Add(nameof(Trailer));
 | 
			
		||||
                    itemTypes.Add(nameof(LiveTvProgram));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                query.IncludeItemTypes = itemTypes.ToArray();
 | 
			
		||||
                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
 | 
			
		||||
            }
 | 
			
		||||
            else if (item is MusicArtist)
 | 
			
		||||
            {
 | 
			
		||||
                query.IncludeItemTypes = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                itemsResult = _libraryManager.GetItemList(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
 | 
			
		||||
 | 
			
		||||
            var result = new QueryResult<BaseItemDto>
 | 
			
		||||
            {
 | 
			
		||||
                Items = returnList,
 | 
			
		||||
                TotalRecordCount = itemsResult.Count
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string[] GetRepresentativeItemTypes(string? contentType)
 | 
			
		||||
        {
 | 
			
		||||
            return contentType switch
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -118,7 +119,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="sortBy">Optional. Key to sort by.</param>
 | 
			
		||||
        /// <param name="sortOrder">Optional. Sort order.</param>
 | 
			
		||||
@ -146,16 +147,15 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] bool? isDisliked,
 | 
			
		||||
            [FromQuery] bool? enableImages,
 | 
			
		||||
            [FromQuery] int? imageTypeLimit,
 | 
			
		||||
            [FromQuery] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] bool? enableUserData,
 | 
			
		||||
            [FromQuery] string? sortBy,
 | 
			
		||||
            [FromQuery] SortOrder? sortOrder,
 | 
			
		||||
            [FromQuery] bool enableFavoriteSorting = false,
 | 
			
		||||
            [FromQuery] bool addCurrentProgram = true)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
@ -239,7 +239,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="isMovie">Optional. Filter for movies.</param>
 | 
			
		||||
        /// <param name="isSeries">Optional. Filter for series.</param>
 | 
			
		||||
@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] string? seriesTimerId,
 | 
			
		||||
            [FromQuery] bool? enableImages,
 | 
			
		||||
            [FromQuery] int? imageTypeLimit,
 | 
			
		||||
            [FromQuery] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] bool? enableUserData,
 | 
			
		||||
            [FromQuery] bool? isMovie,
 | 
			
		||||
            [FromQuery] bool? isSeries,
 | 
			
		||||
@ -274,8 +274,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] bool? isLibraryItem,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                IsKids = isKids,
 | 
			
		||||
                IsSports = isSports,
 | 
			
		||||
                IsLibraryItem = isLibraryItem,
 | 
			
		||||
                Fields = RequestHelpers.GetItemFields(fields),
 | 
			
		||||
                Fields = fields,
 | 
			
		||||
                ImageTypeLimit = imageTypeLimit,
 | 
			
		||||
                EnableImages = enableImages
 | 
			
		||||
            }, dtoOptions);
 | 
			
		||||
@ -316,7 +315,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="enableImages">Optional. Include image information in output.</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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
 | 
			
		||||
        /// <response code="200">Live tv recordings returned.</response>
 | 
			
		||||
@ -350,8 +349,8 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            [FromQuery] string? seriesTimerId,
 | 
			
		||||
            [FromQuery] bool? enableImages,
 | 
			
		||||
            [FromQuery] int? imageTypeLimit,
 | 
			
		||||
            [FromQuery] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] bool? enableUserData,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
@ -530,7 +529,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <param name="enableUserData">Optional. Include user data.</param>
 | 
			
		||||
        /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
 | 
			
		||||
        /// <param name="librarySeriesId">Optional. Filter by library series id.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
 | 
			
		||||
        /// <response code="200">Live tv epgs returned.</response>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
@ -561,11 +560,11 @@ 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,
 | 
			
		||||
            [FromQuery] string? fields,
 | 
			
		||||
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
            var user = userId.HasValue && !userId.Equals(Guid.Empty)
 | 
			
		||||
@ -606,8 +605,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
            return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
@ -662,8 +660,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(body.Fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = body.Fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
 | 
			
		||||
            return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
@ -685,7 +682,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
        /// <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="genreIds">The genres to return guide information for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
 | 
			
		||||
        /// <param name="enableUserData">Optional. include user data.</param>
 | 
			
		||||
        /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
 | 
			
		||||
        /// <response code="200">Recommended epgs returned.</response>
 | 
			
		||||
@ -705,9 +702,9 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
            [FromQuery] bool? enableUserData,
 | 
			
		||||
            [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
        {
 | 
			
		||||
@ -729,8 +726,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                GenreIds = RequestHelpers.GetGuids(genreIds)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var dtoOptions = new DtoOptions()
 | 
			
		||||
                .AddItemFields(fields)
 | 
			
		||||
            var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
                .AddClientFields(Request)
 | 
			
		||||
                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
            return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
 | 
			
		||||
@ -1077,7 +1073,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            var client = _httpClientFactory.CreateClient(NamedClient.Default);
 | 
			
		||||
            // https://json.schedulesdirect.org/20141201/available/countries
 | 
			
		||||
            // Can't dispose the response as it's required up the call chain.
 | 
			
		||||
            var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
 | 
			
		||||
            var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
 | 
			
		||||
@ -1220,11 +1216,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()
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user