Merge branch 'master' into fix-hdhomerun
This commit is contained in:
commit
8204b25a56
|
@ -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
|
||||
|
|
59
.ci/azure-pipelines-api-client.yml
Normal file
59
.ci/azure-pipelines-api-client.yml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
1
.gitignore
vendored
|
@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
|
|||
web/
|
||||
web-src.*
|
||||
MediaBrowser.WebDashboard/jellyfin-web
|
||||
apiclient/generated
|
||||
|
|
3
.npmrc
Normal file
3
.npmrc
Normal 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
10
.vscode/launch.json
vendored
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG DOTNET_VERSION=3.1
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
|
@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& yarn install \
|
||||
&& mv dist /dist
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=3.1
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
|
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& mv dist /dist
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#####################################
|
||||
# Requires binfm_misc registration
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=3.1
|
||||
ARG DOTNET_VERSION=5.0
|
||||
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
|
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
|
|||
&& mv dist /dist
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace DvdLib.Ifo
|
|||
continue;
|
||||
}
|
||||
|
||||
var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
|
||||
{
|
||||
ReadVTS(ifoNumber, ifo.FullName);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
foreach (var att in profile.XmlRootAttributes)
|
||||
{
|
||||
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing
|
|||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
// Starts with SECOND-
|
||||
header = header.Split('-').Last();
|
||||
header = header.Split('-')[^1];
|
||||
|
||||
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
|
|
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 |
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace Emby.Drawing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -15,6 +15,11 @@ namespace Emby.Naming.Video
|
|||
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
|
||||
{
|
||||
CleanDateTimeResult result = new CleanDateTimeResult(name);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var len = cleanDateTimeRegexes.Count;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -83,7 +83,7 @@ namespace Emby.Notifications
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
|
||||
private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var type = NotificationType.ServerRestartRequired.ToString();
|
||||
|
||||
|
@ -99,7 +99,7 @@ namespace Emby.Notifications
|
|||
await SendNotification(notification, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
{
|
||||
var entry = e.Argument;
|
||||
|
||||
|
@ -132,7 +132,7 @@ namespace Emby.Notifications
|
|||
return _config.GetConfiguration<NotificationOptions>("notifications");
|
||||
}
|
||||
|
||||
private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
|
||||
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_appHost.HasUpdateAvailable)
|
||||
{
|
||||
|
@ -151,7 +151,7 @@ namespace Emby.Notifications
|
|||
await SendNotification(notification, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ namespace Emby.Notifications
|
|||
return item.SourceType == SourceType.Library;
|
||||
}
|
||||
|
||||
private async void LibraryUpdateTimerCallback(object state)
|
||||
private async void LibraryUpdateTimerCallback(object? state)
|
||||
{
|
||||
List<BaseItem> items;
|
||||
|
||||
|
@ -209,7 +209,10 @@ namespace Emby.Notifications
|
|||
_libraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
items = items.Take(10).ToList();
|
||||
if (items.Count > 10)
|
||||
{
|
||||
items = items.GetRange(0, 10);
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
|
@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
}
|
||||
catch (Exception)
|
||||
{
|
||||
configuration = Activator.CreateInstance(type);
|
||||
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||
|
@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
|
|||
// If the file didn't exist before, or if something has changed, re-save
|
||||
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
// Save it after load in case we got new items
|
||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-->
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
if (auth?.User == null)
|
||||
if (!auth.IsAuthenticated)
|
||||
{
|
||||
return null;
|
||||
throw new AuthenticationException("Invalid token.");
|
||||
}
|
||||
|
||||
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var (authInfo, _) =
|
||||
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
|
@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var (authInfo, originalAuthInfo) =
|
||||
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
if (originalAuthInfo != null)
|
||||
{
|
||||
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||
}
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||
in Dictionary<string, string> auth,
|
||||
in IHeaderDictionary headers,
|
||||
in IQueryCollection queryString)
|
||||
|
@ -108,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
if (string.IsNullOrEmpty(currentFile))
|
||||
{
|
||||
return (files.Last(), true);
|
||||
return (files[^1], true);
|
||||
}
|
||||
|
||||
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
|
||||
|
|
|
@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost)
|
||||
.Parse(info, channelIdPrefix, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
|
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
|
||||
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
|
||||
{
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
|
||||
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, channelIdPrefix, tunerHostId);
|
||||
return GetChannels(reader, channelIdPrefix, info.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
}
|
||||
}
|
||||
|
||||
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
|
||||
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetStreamAsync(url);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
if (!string.IsNullOrEmpty(info.UserAgent))
|
||||
{
|
||||
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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 () =>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "المجلدات",
|
||||
"Genres": "التضنيفات",
|
||||
"HeaderAlbumArtists": "فناني الألبومات",
|
||||
"HeaderCameraUploads": "تحميلات الكاميرا",
|
||||
"HeaderContinueWatching": "استئناف",
|
||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Папки",
|
||||
"Genres": "Жанрове",
|
||||
"HeaderAlbumArtists": "Изпълнители на албуми",
|
||||
"HeaderCameraUploads": "Качени от камера",
|
||||
"HeaderContinueWatching": "Продължаване на гледането",
|
||||
"HeaderFavoriteAlbums": "Любими албуми",
|
||||
"HeaderFavoriteArtists": "Любими изпълнители",
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
|
||||
"HeaderAlbumArtists": "এলবাম শিল্পী",
|
||||
"Genres": "জেনার",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Mapper",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albumkunstnere",
|
||||
"HeaderCameraUploads": "Kamera Uploads",
|
||||
"HeaderContinueWatching": "Fortsæt Afspilning",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Φάκελοι",
|
||||
"Genres": "Είδη",
|
||||
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
|
||||
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
|
||||
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
|
||||
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
|
||||
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user