Merge branch 'master' into fix-hdhomerun

This commit is contained in:
Ryan Petris 2020-11-15 19:55:30 -07:00
commit 8204b25a56
No known key found for this signature in database
GPG Key ID: C3D15EFA013E18C2
548 changed files with 7079 additions and 9749 deletions

View File

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 3.1.100
default: 5.0.100
jobs:
- job: CompatibilityCheck
@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact'
enabled: false
inputs:
source: "specific"
artifact: "$(NugetPackageName)"
@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact'
enabled: false
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll'
@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool'
enabled: false
inputs:
command: custom
custom: compat

View File

@ -0,0 +1,59 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.0-beta2"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
dependsOn: Test
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec Artifact'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: CmdLine@2
displayName: 'Download OpenApi Generator'
inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
## Authenticate with npm registry
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
## Generate npm api client
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
## Run npm install
- task: Npm@1
displayName: 'Install npm dependencies'
inputs:
command: install
workingDir: ./apiclient/generated/typescript/axios
## Publish npm packages
- task: Npm@1
displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: publish
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

View File

@ -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

View File

@ -65,6 +65,38 @@ jobs:
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
displayName: 'Push OpenAPI Spec to repository'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
contents: 'openapi.json'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
- job: BuildDocker
displayName: 'Build Docker'
@ -135,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'
@ -144,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'

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 3.1.100
default: 5.0.100
jobs:
- job: Test
@ -56,7 +56,7 @@ jobs:
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"
@ -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'

View File

@ -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
@ -35,6 +35,12 @@ jobs:
Windows: 'windows-latest'
macOS: 'macos-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
parameters:
@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

1
.gitignore vendored
View File

@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

10
.vscode/launch.json vendored
View File

@ -6,19 +6,23 @@
"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",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
"internalConsoleOptions": "openOnSessionStart",
"serverReadyAction": {
"action": "openExternally",
"pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'",
}
},
{
"name": ".NET Core Launch (nowebclient)",
"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",

View File

@ -103,6 +103,7 @@
- [sl1288](https://github.com/sl1288)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@ -135,6 +136,8 @@
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
# Emby Contributors

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -10,7 +10,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -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);

View File

@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
User = user,
Recursive = true,
IsMissing = false,
ExcludeItemTypes = new[] { typeof(Book).Name },
ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
ExcludeItemTypes = new[] { typeof(Book).Name },
ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false,
DtoOptions = GetDtoOptions()
};
@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex,
Limit = limit,
};
query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false);
@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Series).Name };
query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Movie).Name };
query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
// query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query);
@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Audio).Name };
query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Audio).Name };
query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query);
@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Series).Name };
query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query);
@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Episode).Name };
query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query);
@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Movie).Name };
query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query);
@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query);
@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name },
IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
},
@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
ArtistIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()
@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
{
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{
stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
id = id.Split(new[] { '_' }, 2)[1];
stubType = Enum.Parse<StubType>(name, true);
id = id.Split('_', 2)[1];
break;
}

View File

@ -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);

View File

@ -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)

View File

@ -126,14 +126,14 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
builder.Append("ModelName:").AppendLine(profile.ModelName);
builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
@ -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))
{

View File

@ -17,7 +17,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -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))
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 278 B

View File

@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints()
{
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses)
{
@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
var device = new SsdpRootDevice

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Xml;
@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger)
: base(config, logger)
{
@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
/// <summary>
/// Records that the handle is authorized in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsAuthorized(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
/// <summary>
/// Records that the handle is validated in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsValidated(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
}

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
/// <summary>
/// Defines the <see cref="MediaReceiverRegistrarService" />.
/// </summary>
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{
private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger,
IHttpClientFactory httpClientFactory,
@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
/// <inheritdoc />
public string GetServiceXml()
{
return new MediaReceiverRegistrarXmlBuilder().GetXml();
return MediaReceiverRegistrarXmlBuilder.GetXml();
}
/// <inheritdoc />

View File

@ -1,79 +1,89 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
public class MediaReceiverRegistrarXmlBuilder
/// <summary>
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
/// </summary>
public static class MediaReceiverRegistrarXmlBuilder
{
public string GetXml()
/// <summary>
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
/// </summary>
/// <returns>An XML representation of this service.</returns>
public static string GetXml()
{
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
/// <summary>
/// The a list of all the state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
var list = new List<StateVariable>();
list.Add(new StateVariable
var list = new List<StateVariable>
{
new StateVariable
{
Name = "AuthorizationGrantedUpdateID",
DataType = "ui4",
SendsEvents = true
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "A_ARG_TYPE_DeviceID",
DataType = "string",
SendsEvents = false
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "AuthorizationDeniedUpdateID",
DataType = "ui4",
SendsEvents = true
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "ValidationSucceededUpdateID",
DataType = "ui4",
SendsEvents = true
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "A_ARG_TYPE_RegistrationRespMsg",
DataType = "bin.base64",
SendsEvents = false
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "A_ARG_TYPE_RegistrationReqMsg",
DataType = "bin.base64",
SendsEvents = false
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "ValidationRevokedUpdateID",
DataType = "ui4",
SendsEvents = true
});
},
list.Add(new StateVariable
new StateVariable
{
Name = "A_ARG_TYPE_Result",
DataType = "int",
SendsEvents = false
});
}
};
return list;
}

View File

@ -1,13 +1,19 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
public class ServiceActionListBuilder
/// <summary>
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{
public IEnumerable<ServiceAction> GetActions()
/// <summary>
/// Returns a list of services that this instance provides.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{
return new[]
{
@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
};
}
/// <summary>
/// Returns the action details for "IsValidated".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsValidated()
{
var action = new ServiceAction
@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
/// <summary>
/// Returns the action details for "IsAuthorized".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsAuthorized()
{
var action = new ServiceAction
@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
/// <summary>
/// Returns the action details for "RegisterDevice".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetRegisterDevice()
{
var action = new ServiceAction
@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
/// <summary>
/// Returns the action details for "GetValidationSucceededUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationSucceededUpdateID()
{
var action = new ServiceAction
@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
private ServiceAction GetGetAuthorizationDeniedUpdateID()
/// <summary>
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
{
var action = new ServiceAction
{
@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
private ServiceAction GetGetValidationRevokedUpdateID()
/// <summary>
/// Returns the action details for "GetValidationRevokedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationRevokedUpdateID()
{
var action = new ServiceAction
{
@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
private ServiceAction GetGetAuthorizationGrantedUpdateID()
/// <summary>
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
{
var action = new ServiceAction
{

View File

@ -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>();
@ -669,9 +669,7 @@ namespace Emby.Dlna.PlayTo
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{
if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
{
switch (commandType)
switch (command.Name)
{
case GeneralCommandType.VolumeDown:
return _device.VolumeDown(cancellationToken);
@ -724,9 +722,6 @@ namespace Emby.Dlna.PlayTo
}
}
return Task.CompletedTask;
}
private async Task SetAudioStreamIndex(int? newIndex)
{
var media = _device.CurrentMediaInfo;
@ -816,7 +811,7 @@ namespace Emby.Dlna.PlayTo
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
@ -828,17 +823,17 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.Play)
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.PlayState)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.GeneralCommand)
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
@ -886,7 +881,10 @@ namespace Emby.Dlna.PlayTo
return null;
}
if (_mediaSourceManager != null)
{
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
}
return mediaSource;
}

View File

@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
SupportedCommands = new[]
{
GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource.ToString()
GeneralCommandType.VolumeDown,
GeneralCommandType.VolumeUp,
GeneralCommandType.Mute,
GeneralCommandType.Unmute,
GeneralCommandType.ToggleMute,
GeneralCommandType.SetVolume,
GeneralCommandType.SetAudioStreamIndex,
GeneralCommandType.SetSubtitleStreamIndex,
GeneralCommandType.PlayMediaSource
},
SupportsMediaControl = true

View File

@ -60,11 +60,9 @@ namespace Emby.Dlna.Service
Async = true
};
using (var reader = XmlReader.Create(streamReader, readerSettings))
{
using var reader = XmlReader.Create(streamReader, readerSettings);
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
}
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@ -124,11 +122,9 @@ namespace Emby.Dlna.Service
{
if (!reader.IsEmptyElement)
{
using (var subReader = reader.ReadSubtree())
{
using var subReader = reader.ReadSubtree();
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
}
else
{
await reader.ReadAsync().ConfigureAwait(false);
@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
}
}
return new ControlRequestInfo();
throw new EndOfStreamException("Stream ended but no body tag found.");
}
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{
var result = new ControlRequestInfo();
string namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@ -165,17 +161,16 @@ namespace Emby.Dlna.Service
{
if (reader.NodeType == XmlNodeType.Element)
{
result.LocalName = reader.LocalName;
result.NamespaceURI = reader.NamespaceURI;
localName = reader.LocalName;
namespaceURI = reader.NamespaceURI;
if (!reader.IsEmptyElement)
{
using (var subReader = reader.ReadSubtree())
{
var result = new ControlRequestInfo(localName, namespaceURI);
using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result;
}
}
else
{
await reader.ReadAsync().ConfigureAwait(false);
@ -187,7 +182,12 @@ namespace Emby.Dlna.Service
}
}
return result;
if (localName != null && namespaceURI != null)
{
return new ControlRequestInfo(localName, namespaceURI);
}
throw new EndOfStreamException("Stream ended but no control found.");
}
private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
@ -234,11 +234,18 @@ namespace Emby.Dlna.Service
private class ControlRequestInfo
{
public ControlRequestInfo(string localName, string namespaceUri)
{
LocalName = localName;
NamespaceURI = namespaceUri;
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, string> Headers { get; }
}
}
}

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -36,7 +36,7 @@ namespace Emby.Drawing
private readonly IImageEncoder _imageEncoder;
private readonly IMediaEncoder _mediaEncoder;
private bool _disposed = false;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ImageProcessor"/> class.
@ -466,11 +466,11 @@ namespace Emby.Drawing
}
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options)
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options);
_imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
}

View File

@ -38,7 +38,7 @@ namespace Emby.Drawing
}
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options)
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
throw new NotImplementedException();
}

View File

@ -1,6 +1,6 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
public AudioBookFilePathParserResult Parse(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
var result = new AudioBookFilePathParserResult();
AudioBookFilePathParserResult result = default;
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{

View File

@ -1,8 +1,9 @@
#nullable enable
#pragma warning disable CS1591
namespace Emby.Naming.AudioBook
{
public class AudioBookFilePathParserResult
public struct AudioBookFilePathParserResult
{
public int? PartNumber { get; set; }

View File

@ -1,3 +1,4 @@
#nullable enable
#pragma warning disable CS1591
using System;
@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
_options = options;
}
public AudioBookFileInfo ParseFile(string path)
public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
{
return Resolve(path, false);
}
public AudioBookFileInfo ParseDirectory(string path)
if (path.Length == 0)
{
return Resolve(path, true);
}
public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
throw new ArgumentException("String can't be empty.", nameof(path));
}
// TODO

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -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++)
{

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -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)
{

View File

@ -19,7 +19,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
}
/// <inheritdoc />
public string VirtualDataPath { get; } = "%AppDataPath%";
public string VirtualDataPath => "%AppDataPath%";
/// <summary>
/// Gets the image cache path.

View File

@ -133,6 +133,33 @@ namespace Emby.Server.Implementations.AppBase
}
}
/// <summary>
/// Manually pre-loads a factory so that it is available pre system initialisation.
/// </summary>
/// <typeparam name="T">Class to register.</typeparam>
public virtual void RegisterConfiguration<T>()
where T : IConfigurationFactory
{
IConfigurationFactory factory = Activator.CreateInstance<T>();
if (_configurationFactories == null)
{
_configurationFactories = new[] { factory };
}
else
{
var oldLen = _configurationFactories.Length;
var arr = new IConfigurationFactory[oldLen + 1];
_configurationFactories.CopyTo(arr, 0);
arr[oldLen] = factory;
_configurationFactories = arr;
}
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
/// <summary>
/// Adds parts.
/// </summary>

View File

@ -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))
{

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Mvc;
@ -127,8 +126,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory;
private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
/// <summary>
@ -339,7 +336,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
@ -403,7 +400,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Resolves this instance.
/// </summary>
/// <typeparam name="T">The type</typeparam>
/// <typeparam name="T">The type.</typeparam>
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
@ -499,24 +496,11 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration.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>
@ -537,6 +521,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager);
@ -665,7 +650,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
_httpClientFactory = Resolve<IHttpClientFactory>();
_webSocketManager = Resolve<IWebSocketManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@ -781,12 +765,25 @@ 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();
_webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@ -815,53 +812,6 @@ namespace Emby.Server.Implementations
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
private IPlugin LoadPlugin(IPlugin plugin)
{
try
{
if (plugin is IPluginAssembly assemblyPlugin)
{
var assembly = plugin.GetType().Assembly;
var assemblyName = assembly.GetName();
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
try
{
var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
if (idAttributes.Length > 0)
{
var attribute = (GuidAttribute)idAttributes[0];
var assemblyId = new Guid(attribute.Value);
assemblyPlugin.SetId(assemblyId);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
}
}
if (plugin is IHasPluginConfiguration hasPluginConfiguration)
{
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
}
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>
@ -872,6 +822,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)
@ -1026,69 +992,60 @@ 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
{
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
// Add it under the path name and version 0.0.0.1.
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
{
// Versioned folder.
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 LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
@ -1098,14 +1055,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;
}
@ -1113,6 +1070,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);
@ -1125,7 +1083,7 @@ namespace Emby.Server.Implementations
}
}
return dllList;
return versions;
}
/// <summary>
@ -1136,7 +1094,9 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
@ -1153,6 +1113,7 @@ namespace Emby.Server.Implementations
yield return plugAss;
}
}
}
// Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly;

View File

@ -1,51 +0,0 @@
using System;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Browser
{
/// <summary>
/// Assists in opening application URLs in an external browser.
/// </summary>
public static class BrowserLauncher
{
/// <summary>
/// Opens the home page of the web client.
/// </summary>
/// <param name="appHost">The app host.</param>
public static void OpenWebApp(IServerApplicationHost appHost)
{
TryOpenUrl(appHost, "/web/index.html");
}
/// <summary>
/// Opens the swagger API page.
/// </summary>
/// <param name="appHost">The app host.</param>
public static void OpenSwaggerPage(IServerApplicationHost appHost)
{
TryOpenUrl(appHost, "/api-docs/swagger");
}
/// <summary>
/// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
{
try
{
string baseUrl = appHost.GetLocalApiUrl("localhost");
appHost.LaunchUrl(baseUrl + relativeUrl);
}
catch (Exception ex)
{
var logger = appHost.Resolve<ILogger<IServerApplicationHost>>();
logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
}
}
}
}

View File

@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels;
var totalCount = all.Count;
if (query.StartIndex.HasValue)
if (query.StartIndex.HasValue || query.Limit.HasValue)
{
all = all.Skip(query.StartIndex.Value).ToList();
int startIndex = query.StartIndex ?? 0;
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
all = all.GetRange(startIndex, count);
}
if (query.Limit.HasValue)
{
all = all.Take(query.Limit.Value).ToList();
}
var returnItems = all.ToArray();
if (query.RefreshLatestChannelItems)
{
foreach (var item in returnItems)
foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel>
{
Items = returnItems,
Items = all,
TotalRecordCount = totalCount
};
}
@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Channel).Name },
IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
}

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Channel).Name },
IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray()
});

View File

@ -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);

View File

@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name)
{
return connection.RunInTransaction(db =>
return connection.RunInTransaction(
db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{

View File

@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value.ToByteArray());
Span<byte> byteValue = stackalloc byte[16];
value.TryWriteBytes(byteValue);
bindParam.Bind(byteValue);
}
else
{

View File

@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{
connection.RunQueries(queries);
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
SaveItemsInTranscation(db, tuples);
}, TransactionMode);
@ -1004,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)
{
@ -1054,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)
{
@ -1093,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)
{
@ -1529,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++;
@ -1590,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))
{
@ -1609,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++;
@ -1619,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++;
@ -1633,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))
{
@ -1808,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++;
@ -1845,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++;
@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
// First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@ -2263,7 +2267,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
@ -2400,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)
@ -2413,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");
@ -2922,7 +2943,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
@ -3291,7 +3313,6 @@ namespace Emby.Server.Implementations.Data
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
@ -3326,7 +3347,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<Guid>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
@ -3910,7 +3932,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{
if (query.IsPlayed.Value)
{
@ -4751,29 +4773,29 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<string>();
if (IsTypeInQuery(typeof(Person).Name, query))
if (IsTypeInQuery(nameof(Person), query))
{
list.Add(typeof(Person).Name);
list.Add(nameof(Person));
}
if (IsTypeInQuery(typeof(Genre).Name, query))
if (IsTypeInQuery(nameof(Genre), query))
{
list.Add(typeof(Genre).Name);
list.Add(nameof(Genre));
}
if (IsTypeInQuery(typeof(MusicGenre).Name, query))
if (IsTypeInQuery(nameof(MusicGenre), query))
{
list.Add(typeof(MusicGenre).Name);
list.Add(nameof(MusicGenre));
}
if (IsTypeInQuery(typeof(MusicArtist).Name, query))
if (IsTypeInQuery(nameof(MusicArtist), query))
{
list.Add(typeof(MusicArtist).Name);
list.Add(nameof(MusicArtist));
}
if (IsTypeInQuery(typeof(Studio).Name, query))
if (IsTypeInQuery(nameof(Studio), query))
{
list.Add(typeof(Studio).Name);
list.Add(nameof(Studio));
}
return list;
@ -4828,12 +4850,12 @@ namespace Emby.Server.Implementations.Data
var types = new[]
{
typeof(Episode).Name,
typeof(Video).Name,
typeof(Movie).Name,
typeof(MusicVideo).Name,
typeof(Series).Name,
typeof(Season).Name
nameof(Episode),
nameof(Video),
nameof(Movie),
nameof(MusicVideo),
nameof(Series),
nameof(Season)
};
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@ -4901,7 +4923,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
connection.ExecuteAll(sql);
}, TransactionMode);
@ -4952,7 +4975,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var idBlob = id.ToByteArray();
@ -4996,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);
@ -5039,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);
@ -5081,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();
@ -5101,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)
{
@ -5118,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)
{
@ -5133,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;
@ -5414,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,
@ -5586,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)
@ -5746,7 +5771,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = itemId.ToByteArray();
@ -5900,7 +5926,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = id.ToByteArray();
@ -6006,7 +6033,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
/// <summary>
/// Gets the chapter.
/// </summary>
@ -6235,7 +6261,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = id.ToByteArray();

View File

@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users;
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
db.ExecuteAll(string.Join(";", new[] {
@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
foreach (var userItemData in userDataList)
{

View File

@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto
continue;
}
var containers = container.Split(new[] { ',' });
var containers = container.Split(',');
if (containers.Length < 2)
{
continue;
@ -465,10 +465,9 @@ namespace Emby.Server.Implementations.Dto
{
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album,
Limit = 1
});
if (parentAlbumIds.Count > 0)
@ -1139,6 +1138,7 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
}
@ -1185,6 +1185,7 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, series);
}
}
}
@ -1431,7 +1432,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
return width / height;
return (double)width / height;
}
}
}

View File

@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.224" />
<PackageReference Include="IPNetwork2" Version="2.5.226" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@ -32,13 +32,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.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
<PackageReference Include="Mono.Nat" Version="2.0.2" />
<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" />
@ -49,10 +49,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-->

View File

@ -16,6 +16,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -105,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
_sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
}
catch
{
@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
_sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
}
catch
{
@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
}
private async Task SendMessage(string name, TimerEventInfo info)
private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{
var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();

View File

@ -1,83 +0,0 @@
using System.Threading.Tasks;
using Emby.Server.Implementations.Browser;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// Class StartupWizard.
/// </summary>
public sealed class StartupWizard : IServerEntryPoint
{
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _appConfig;
private readonly IServerConfigurationManager _config;
private readonly IStartupOptions _startupOptions;
/// <summary>
/// Initializes a new instance of the <see cref="StartupWizard"/> class.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="appConfig">The application configuration.</param>
/// <param name="config">The configuration manager.</param>
/// <param name="startupOptions">The application startup options.</param>
public StartupWizard(
IServerApplicationHost appHost,
IConfiguration appConfig,
IServerConfigurationManager config,
IStartupOptions startupOptions)
{
_appHost = appHost;
_appConfig = appConfig;
_config = config;
_startupOptions = startupOptions;
}
/// <inheritdoc />
public Task RunAsync()
{
Run();
return Task.CompletedTask;
}
private void Run()
{
if (!_appHost.CanLaunchWebBrowser)
{
return;
}
// Always launch the startup wizard if possible when it has not been completed
if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient())
{
BrowserLauncher.OpenWebApp(_appHost);
return;
}
// Do nothing if the web app is configured to not run automatically
if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp)
{
return;
}
// Launch the swagger page if the web client is not hosted, otherwise open the web client
if (_appConfig.HostWebClient())
{
BrowserLauncher.OpenWebApp(_appHost);
}
else
{
BrowserLauncher.OpenSwaggerPage(_appHost);
}
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@ -28,7 +28,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly object _syncLock = new object();
private Timer _updateTimer;
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
_userDataManager = userDataManager;
@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
{
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)

View File

@ -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.");
}

View File

@ -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,18 +101,27 @@ 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))
{
// Request doesn't contain a token.
return authInfo;
}
var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (result.Items.Count > 0)
{
authInfo.IsAuthenticated = true;
}
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (originalAuthenticationInfo != null)
{
@ -180,6 +182,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
authInfo.IsApiKey = true;
}
else
{
authInfo.IsApiKey = false;
}
if (updateToken)
@ -187,9 +195,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
_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,12 +269,12 @@ 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[] { '"' }));
result.Add(param[0], value);
var value = NormalizeValue(param[1].Trim('"'));
result[param[0]] = value;
}
}

View File

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
Connection = this
};
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
if (info.MessageType == SessionMessageType.KeepAlive)
{
await SendKeepAliveResponse().ConfigureAwait(false);
}
@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
new WebSocketMessage<string>
{
MessageId = Guid.NewGuid(),
MessageType = "KeepAlive"
MessageType = SessionMessageType.KeepAlive
}, CancellationToken.None);
}

View File

@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
{
public class WebSocketManager : IWebSocketManager
{
private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public WebSocketManager(
Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners;
_logger = logger;
_loggerFactory = loggerFactory;
}
@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
/// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="listeners">The web socket listeners.</param>
public void Init(IEnumerable<IWebSocketListener> listeners)
{
_webSocketListeners = listeners.ToArray();
}
/// <summary>
/// Processes the web socket message received.
/// </summary>
@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
IEnumerable<Task> GetTasks()
{
foreach (var x in _webSocketListeners)
var listeners = _webSocketListeners.Value;
foreach (var x in listeners)
{
yield return x.ProcessMessageAsync(result);
}

View File

@ -16,11 +16,6 @@ namespace Emby.Server.Implementations
/// </summary>
bool IsService { get; }
/// <summary>
/// Gets the value of the --noautorunwebapp command line option.
/// </summary>
bool NoAutoRunWebApp { get; }
/// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>

View File

@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
// return _libraryManager.GetItemList(new InternalItemsQuery
// {
// ArtistIds = new[] { item.Id },
// IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
// IncludeItemTypes = new[] { nameof(MusicAlbum) },
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
// Limit = 4,
// Recursive = true,

View File

@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
{
var useBackdrop = primaryItem is CollectionFolder;
return items
.Select(i =>
{
// Use Backdrop instead of Primary image for Library images.
if (useBackdrop)
{
var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
if (backdrop != null && backdrop.IsLocalFile)
{
return backdrop.Path;
}
}
var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile)
{
@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
return null;
}
ImageProcessor.CreateImageCollage(options);
ImageProcessor.CreateImageCollage(options, primaryItem.Name);
return outputPath;
}

View File

@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name },
IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,

View File

@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
public BaseItem GetParentItem(string parentId, Guid? userId)
{
if (!string.IsNullOrEmpty(parentId))
{
return GetItemById(new Guid(parentId));
}
if (userId.HasValue && userId != Guid.Empty)
{
return GetUserRootFolder();
}
return RootFolder;
}
/// <inheritdoc />
public bool IsVideoFile(string path)
{
@ -2690,7 +2705,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)
{

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private IMediaSourceProvider[] _providers;
@ -582,11 +583,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
var info = _openStreams.Values.FirstOrDefault(i =>
{
@ -599,12 +596,7 @@ namespace Emby.Server.Implementations.Library
return false;
});
return info as IDirectStreamProvider;
}
finally
{
_liveStreamSemaphore.Release();
}
return Task.FromResult(info as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
}
private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
return info;
return Task.FromResult(info);
}
else
{
throw new ResourceNotFoundException();
}
}
finally
{
_liveStreamSemaphore.Release();
return Task.FromException<ILiveStream>(new ResourceNotFoundException());
}
}
@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
if (liveStream.ConsumerCount <= 0)
{
_openStreams.Remove(id);
_openStreams.TryRemove(id, out _);
_logger.LogInformation("Closing live stream {0}", id);
@ -866,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));

View File

@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(Audio).Name },
IncludeItemTypes = new[] { nameof(Audio) },
DtoOptions = dtoOptions
})
.Cast<Audio>()
@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(Audio).Name },
IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = genreIds.ToArray(),

View File

@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth;
public MultiItemResolverResult ResolveMultiple(Folder parent,
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result;
}
private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
@ -199,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
{

View File

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@ -113,22 +116,29 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
// check for audio files before digging down into directories
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
if (foundAudioFile)
{
// at least one audio file exists
return true;
}
if (!allowSubfolders)
{
// not music since no audio file exists and we're not looking into subfolders
return false;
}
var discSubfolderCount = 0;
var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
foreach (var fileSystemInfo in list)
{
if (fileSystemInfo.IsDirectory)
{
if (allowSubfolders)
{
if (notMultiDisc)
{
continue;
}
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
var path = fileSystemInfo.FullName;
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
@ -137,28 +147,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (parser.IsMultiPart(path))
{
logger.LogDebug("Found multi-disc folder: " + path);
discSubfolderCount++;
Interlocked.Increment(ref discSubfolderCount);
}
else
{
// If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
notMultiDisc = true;
state.Stop();
}
}
}
}
else
{
var fullName = fileSystemInfo.FullName;
});
if (libraryManager.IsAudioFile(fullName))
{
return true;
}
}
}
if (notMultiDisc)
if (!result.IsCompleted)
{
return false;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder
return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
{
// stop once we see a music album
state.Stop();
}
});
return !result.IsCompleted ? new MusicArtist() : null;
}
}
}

View File

@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty;
return _validExtensions.Contains(fileExtension,
return _validExtensions.Contains(
fileExtension,
StringComparer
.OrdinalIgnoreCase);
}).ToList();

View File

@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = query.ExcludeItemTypes.ToList();
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
excludeItemTypes.Add(typeof(Year).Name);
excludeItemTypes.Add(typeof(Folder).Name);
excludeItemTypes.Add(nameof(Year));
excludeItemTypes.Add(nameof(Folder));
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
AddIfMissing(includeItemTypes, typeof(Genre).Name);
AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
AddIfMissing(includeItemTypes, nameof(Genre));
AddIfMissing(includeItemTypes, nameof(MusicGenre));
}
}
else
{
AddIfMissing(excludeItemTypes, typeof(Genre).Name);
AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
AddIfMissing(excludeItemTypes, nameof(Genre));
AddIfMissing(excludeItemTypes, nameof(MusicGenre));
}
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
AddIfMissing(includeItemTypes, typeof(Person).Name);
AddIfMissing(includeItemTypes, nameof(Person));
}
}
else
{
AddIfMissing(excludeItemTypes, typeof(Person).Name);
AddIfMissing(excludeItemTypes, nameof(Person));
}
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
AddIfMissing(includeItemTypes, typeof(Studio).Name);
AddIfMissing(includeItemTypes, nameof(Studio));
}
}
else
{
AddIfMissing(excludeItemTypes, typeof(Studio).Name);
AddIfMissing(excludeItemTypes, nameof(Studio));
}
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
{
if (!query.IncludeMedia)
{
AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
AddIfMissing(includeItemTypes, nameof(MusicArtist));
}
}
else
{
AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
AddIfMissing(excludeItemTypes, nameof(MusicArtist));
}
AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
AddIfMissing(excludeItemTypes, typeof(Folder).Name);
AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
AddIfMissing(excludeItemTypes, nameof(Folder));
var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0)

View File

@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(MusicArtist).Name },
IncludeItemTypes = new[] { nameof(MusicArtist) },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();

View File

@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Person).Name },
IncludeItemTypes = new[] { nameof(Person) },
IsDeadPerson = true,
IsLocked = false
});

View File

@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Studio).Name },
IncludeItemTypes = new[] { nameof(Studio) },
IsDeadStudio = true,
IsLocked = false
});

View File

@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Series).Name },
IncludeItemTypes = new[] { nameof(Series) },
Name = program.Name
}).ToArray();
@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Episode).Name },
IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,

View File

@ -15,6 +15,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost)
IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_cryptoProvider = cryptoProvider;
}
private string UserAgent => _appHost.ApplicationUserAgent;
@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken)
{
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
string hashedPassword = Hex.Encode(hashedPasswordBytes);
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
@ -874,7 +880,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<Lineup> lineups { get; set; }
}
public class Headends
{
public string headend { get; set; }
@ -886,8 +891,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<Lineup> lineups { get; set; }
}
public class Map
{
public string stationID { get; set; }
@ -971,9 +974,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<string> date { get; set; }
}
public class Rating
{
public string body { get; set; }
@ -1017,8 +1017,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public string isPremiereOrFinale { get; set; }
}
public class MetadataSchedule
{
public string modified { get; set; }

View File

@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(Series).Name },
IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(Series).Name },
IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(Series).Name },
IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
{
program = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },

View File

@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase)
{
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
}
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsMovie.Value)
{
includeItemTypes.Add(typeof(Movie).Name);
includeItemTypes.Add(nameof(Movie));
}
else
{
excludeItemTypes.Add(typeof(Movie).Name);
excludeItemTypes.Add(nameof(Movie));
}
}
@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsSeries.Value)
{
includeItemTypes.Add(typeof(Episode).Name);
includeItemTypes.Add(nameof(Episode));
}
else
{
excludeItemTypes.Add(typeof(Episode).Name);
excludeItemTypes.Add(nameof(Episode));
}
}
@ -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);
@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
@ -2135,6 +2135,7 @@ namespace Emby.Server.Implementations.LiveTv
}
private bool _disposed = false;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@ -2207,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));

View File

@ -563,6 +563,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
if (tunerCount > 0)
{
var tunerHostId = info.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount)
{
throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached.");
}
}
var profile = streamId.Split('_')[0];
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);

View File

@ -135,6 +135,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
public string GetFilePath()
{
return TempFilePath;
}
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
using (udpClient)

View File

@ -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;

View File

@ -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))
{
}
}

View File

@ -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);
}
return Task.FromResult((Stream)File.OpenRead(url));
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(requestMessage, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
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

View File

@ -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 () =>

View File

@ -1,19 +1,19 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
"Folders": "Fouers",
"Favorites": "Gunstelinge",
"Folders": "Lêergidse",
"Favorites": "Gunstellinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Rolprente",
"Shows": "Program",
"HeaderContinueWatching": "Hou Aan Kyk",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
"HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
"Playlists": "Speellysse",
"Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
@ -23,7 +23,7 @@
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
"HeaderLiveTV": "Live TV",
"HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
@ -85,7 +85,6 @@
"ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames",
"HeaderRecordingGroups": "Groep Opnames",
"HeaderCameraUploads": "Kamera Oplaai",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk",
@ -95,5 +94,23 @@
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
"TasksMaintenanceCategory": "onderhoud"
"TasksMaintenanceCategory": "onderhoud",
"TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
"TaskCleanCache": "Reinig Kasgeheue Lêergids",
"TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
"TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
"TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
"TaskRefreshChannels": "Vervris Kanale",
"TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
"TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
"TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
"TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
"TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
"TaskRefreshPeople": "Vervris Mense",
"TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
"TaskCleanLogs": "Reinig Loglêer Lêervouer",
"TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
"TaskRefreshLibrary": "Skandeer Media Versameling",
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
}

View File

@ -16,7 +16,6 @@
"Folders": "المجلدات",
"Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات",
"HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",

View File

@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
"HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",

View File

@ -14,7 +14,6 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",

View File

@ -16,7 +16,6 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum",
"HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Preferits",

View File

@ -16,7 +16,6 @@
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
"HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
@ -114,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"
}

View File

@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
"HeaderCameraUploads": "Kamera Uploads",
"HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",

View File

@ -16,7 +16,6 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
"HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Fortsetzen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
@ -114,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"
}

View File

@ -16,7 +16,6 @@
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",

View File

@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
@ -114,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"
}

View File

@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
@ -96,6 +95,8 @@
"TasksLibraryCategory": "Library",
"TasksApplicationCategory": "Application",
"TasksChannelsCategory": "Internet Channels",
"TaskCleanActivityLog": "Clean Activity Log",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanCache": "Clean Cache Directory",
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",

Some files were not shown because too many files have changed in this diff Show More