Merge branch 'master' into PluginConfigSave

This commit is contained in:
BaronGreenback 2020-11-19 09:04:49 +00:00 committed by GitHub
commit ef05485243
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
605 changed files with 9671 additions and 10781 deletions

View File

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

View File

@ -0,0 +1,58 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.0-beta2"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
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'
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'
inputs:
command: publish
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

View File

@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 3.1.100
DotNetSdkVersion: 5.0.100
jobs:
- job: Build

View File

@ -65,6 +65,38 @@ jobs:
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
displayName: 'Push OpenAPI Spec to repository'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
contents: 'openapi.json'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
- job: BuildDocker
displayName: 'Build Docker'
@ -135,7 +167,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
@ -144,7 +176,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
@ -156,6 +188,12 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk'
inputs:
packageType: 'sdk'
version: '5.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

View File

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 3.1.100
default: 5.0.100
jobs:
- job: Test
@ -56,7 +56,7 @@ jobs:
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"
@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'

View File

@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
value: 3.1.100
value: 5.0.100
pr:
autoCancel: true
@ -34,6 +34,12 @@ jobs:
Linux: 'ubuntu-latest'
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
@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

1
.gitignore vendored
View File

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

3
.npmrc Normal file
View File

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

10
.vscode/launch.json vendored
View File

@ -6,19 +6,23 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
"internalConsoleOptions": "openOnSessionStart",
"serverReadyAction": {
"action": "openExternally",
"pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'",
}
},
{
"name": ".NET Core Launch (nowebclient)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",

View File

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

View File

@ -1,4 +1,4 @@
ARG DOTNET_VERSION=3.1
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& yarn install \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View File

@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View File

@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=3.1
ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

View File

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

View File

@ -31,7 +31,7 @@ namespace DvdLib.Ifo
continue;
}
var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{
ReadVTS(ifoNumber, ifo.FullName);

View File

@ -1,13 +1,23 @@
#pragma warning disable CS1591
namespace Emby.Dlna.Common
{
/// <summary>
/// DLNA Query parameter type, used when quering DLNA devices via SOAP.
/// </summary>
public class Argument
{
public string Name { get; set; }
/// <summary>
/// Gets or sets name of the DLNA argument.
/// </summary>
public string Name { get; set; } = string.Empty;
public string Direction { get; set; }
/// <summary>
/// Gets or sets the direction of the parameter.
/// </summary>
public string Direction { get; set; } = string.Empty;
public string RelatedStateVariable { get; set; }
/// <summary>
/// Gets or sets the related DLNA state variable for this argument.
/// </summary>
public string RelatedStateVariable { get; set; } = string.Empty;
}
}

View File

@ -1,29 +1,41 @@
#pragma warning disable CS1591
using System.Globalization;
namespace Emby.Dlna.Common
{
/// <summary>
/// Defines the <see cref="DeviceIcon" />.
/// </summary>
public class DeviceIcon
{
public string Url { get; set; }
/// <summary>
/// Gets or sets the Url.
/// </summary>
public string Url { get; set; } = string.Empty;
public string MimeType { get; set; }
/// <summary>
/// Gets or sets the MimeType.
/// </summary>
public string MimeType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the Width.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the Height.
/// </summary>
public int Height { get; set; }
public string Depth { get; set; }
/// <summary>
/// Gets or sets the Depth.
/// </summary>
public string Depth { get; set; } = string.Empty;
/// <inheritdoc />
public override string ToString()
{
return string.Format(
CultureInfo.InvariantCulture,
"{0}x{1}",
Height,
Width);
return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
}
}
}

View File

@ -1,21 +1,36 @@
#pragma warning disable CS1591
namespace Emby.Dlna.Common
{
/// <summary>
/// Defines the <see cref="DeviceService" />.
/// </summary>
public class DeviceService
{
public string ServiceType { get; set; }
/// <summary>
/// Gets or sets the Service Type.
/// </summary>
public string ServiceType { get; set; } = string.Empty;
public string ServiceId { get; set; }
/// <summary>
/// Gets or sets the Service Id.
/// </summary>
public string ServiceId { get; set; } = string.Empty;
public string ScpdUrl { get; set; }
/// <summary>
/// Gets or sets the Scpd Url.
/// </summary>
public string ScpdUrl { get; set; } = string.Empty;
public string ControlUrl { get; set; }
/// <summary>
/// Gets or sets the Control Url.
/// </summary>
public string ControlUrl { get; set; } = string.Empty;
public string EventSubUrl { get; set; }
/// <summary>
/// Gets or sets the EventSubUrl.
/// </summary>
public string EventSubUrl { get; set; } = string.Empty;
/// <inheritdoc />
public override string ToString()
=> ServiceId;
public override string ToString() => ServiceId;
}
}

View File

@ -1,24 +1,31 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
/// <summary>
/// Defines the <see cref="ServiceAction" />.
/// </summary>
public class ServiceAction
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
/// </summary>
public ServiceAction()
{
ArgumentList = new List<Argument>();
}
public string Name { get; set; }
/// <summary>
/// Gets or sets the name of the action.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets the ArgumentList.
/// </summary>
public List<Argument> ArgumentList { get; }
/// <inheritdoc />
public override string ToString()
{
return Name;
}
public override string ToString() => Name;
}
}

View File

@ -1,27 +1,34 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
/// <summary>
/// Defines the <see cref="StateVariable" />.
/// </summary>
public class StateVariable
{
public StateVariable()
{
AllowedValues = Array.Empty<string>();
}
/// <summary>
/// Gets or sets the name of the state variable.
/// </summary>
public string Name { get; set; } = string.Empty;
public string Name { get; set; }
public string DataType { get; set; }
/// <summary>
/// Gets or sets the data type of the state variable.
/// </summary>
public string DataType { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether it sends events.
/// </summary>
public bool SendsEvents { get; set; }
public IReadOnlyList<string> AllowedValues { get; set; }
/// <summary>
/// Gets or sets the allowed values range.
/// </summary>
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
/// <inheritdoc />
public override string ToString()
=> Name;
public override string ToString() => Name;
}
}

View File

@ -19,6 +19,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory
{
/// <summary>
/// Defines the <see cref="ContentDirectoryService" />.
/// </summary>
public class ContentDirectoryService : BaseService, IContentDirectory
{
private readonly ILibraryManager _libraryManager;
@ -33,6 +36,22 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
/// <summary>
/// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
/// </summary>
/// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
/// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager,
@ -62,7 +81,10 @@ namespace Emby.Dlna.ContentDirectory
_tvSeriesManager = tvSeriesManager;
}
private int SystemUpdateId
/// <summary>
/// Gets the system id. (A unique id which changes on when our definition changes.)
/// </summary>
private static int SystemUpdateId
{
get
{
@ -75,14 +97,18 @@ namespace Emby.Dlna.ContentDirectory
/// <inheritdoc />
public string GetServiceXml()
{
return new ContentDirectoryXmlBuilder().GetXml();
return ContentDirectoryXmlBuilder.GetXml();
}
/// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
@ -107,6 +133,11 @@ namespace Emby.Dlna.ContentDirectory
.ProcessControlRequestAsync(request);
}
/// <summary>
/// Get the user stored in the device profile.
/// </summary>
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <returns>The <see cref="User"/>.</returns>
private User GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))

View File

@ -6,143 +6,154 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ContentDirectory
{
public class ContentDirectoryXmlBuilder
/// <summary>
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
/// </summary>
public static class ContentDirectoryXmlBuilder
{
public string GetXml()
/// <summary>
/// Gets the ContentDirectory:1 service template.
/// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
/// </summary>
/// <returns>An XML description of this service.</returns>
public static string GetXml()
{
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
/// <summary>
/// Get the list of 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>
{
Name = "A_ARG_TYPE_Filter",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_Filter",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_SortCriteria",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_SortCriteria",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Index",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_Index",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Count",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_Count",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_UpdateID",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_UpdateID",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "SearchCapabilities",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "SearchCapabilities",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "SortCapabilities",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "SortCapabilities",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "SystemUpdateID",
DataType = "ui4",
SendsEvents = true
});
new StateVariable
{
Name = "SystemUpdateID",
DataType = "ui4",
SendsEvents = true
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_SearchCriteria",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_SearchCriteria",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Result",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_Result",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ObjectID",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_ObjectID",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_BrowseFlag",
DataType = "string",
SendsEvents = false,
new StateVariable
{
Name = "A_ARG_TYPE_BrowseFlag",
DataType = "string",
SendsEvents = false,
AllowedValues = new[]
AllowedValues = new[]
{
"BrowseMetadata",
"BrowseDirectChildren"
}
});
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_BrowseLetter",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_BrowseLetter",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_CategoryType",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_CategoryType",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_RID",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_RID",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_PosSec",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_PosSec",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Featurelist",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_Featurelist",
DataType = "string",
SendsEvents = false
}
};
return list;
}

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,15 @@ using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
/// <summary>
/// Defines the <see cref="ServerItem" />.
/// </summary>
internal class ServerItem
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerItem"/> class.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
public ServerItem(BaseItem item)
{
Item = item;
@ -16,8 +23,14 @@ namespace Emby.Dlna.ContentDirectory
}
}
/// <summary>
/// Gets or sets the underlying base item.
/// </summary>
public BaseItem Item { get; set; }
/// <summary>
/// Gets or sets the DLNA item type.
/// </summary>
public StubType? StubType { get; set; }
}
}

View File

@ -1,13 +1,18 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Common;
namespace Emby.Dlna.ContentDirectory
{
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[]
{
@ -22,6 +27,10 @@ namespace Emby.Dlna.ContentDirectory
};
}
/// <summary>
/// Returns the action details for "GetSystemUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetSystemUpdateIDAction()
{
var action = new ServiceAction
@ -39,6 +48,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
/// <summary>
/// Returns the action details for "GetSearchCapabilities".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchCapabilitiesAction()
{
var action = new ServiceAction
@ -56,6 +69,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
/// <summary>
/// Returns the action details for "GetSortCapabilities".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSortCapabilitiesAction()
{
var action = new ServiceAction
@ -73,6 +90,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
/// <summary>
/// Returns the action details for "X_GetFeatureList".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetX_GetFeatureListAction()
{
var action = new ServiceAction
@ -90,6 +111,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
/// <summary>
/// Returns the action details for "Search".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchAction()
{
var action = new ServiceAction
@ -170,7 +195,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
private ServiceAction GetBrowseAction()
/// <summary>
/// Returns the action details for "Browse".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetBrowseAction()
{
var action = new ServiceAction
{
@ -250,7 +279,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
private ServiceAction GetBrowseByLetterAction()
/// <summary>
/// Returns the action details for "X_BrowseByLetter".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetBrowseByLetterAction()
{
var action = new ServiceAction
{
@ -337,7 +370,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
private ServiceAction GetXSetBookmarkAction()
/// <summary>
/// Returns the action details for "X_SetBookmark".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetXSetBookmarkAction()
{
var action = new ServiceAction
{

View File

@ -3,6 +3,9 @@
namespace Emby.Dlna.ContentDirectory
{
/// <summary>
/// Defines the DLNA item types.
/// </summary>
public enum StubType
{
Folder = 0,

View File

@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{
foreach (var att in profile.XmlRootAttributes)
{
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);

View File

@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl
{
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
_fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
}
public bool Contains(string field)

View File

@ -383,9 +383,9 @@ namespace Emby.Dlna
continue;
}
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
var path = Path.Combine(systemProfilesPath, filename);
var path = Path.Join(
systemProfilesPath,
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name))
{

View File

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

View File

@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
header = header.Split('-').Last();
header = header.Split('-')[^1];
if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
{
@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>");
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
_logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
var startIndex = command.StartIndex ?? 0;
if (startIndex > 0)
{
items = items.Skip(startIndex).ToList();
items = items.GetRange(startIndex, items.Count - startIndex);
}
var playlist = new List<PlaylistItem>();
@ -811,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)
{
@ -823,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);
}
@ -881,7 +881,10 @@ namespace Emby.Dlna.PlayTo
return null;
}
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
if (_mediaSourceManager != null)
{
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
}
return mediaSource;
}
@ -942,7 +945,10 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
// Be careful, IsDirectStream==true by default (Static != false or not in query).
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");

View File

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

View File

@ -45,7 +45,7 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
@ -94,7 +94,7 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
@ -9,15 +6,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
/// <summary>
/// Helper class to determine if Album is multipart.
/// </summary>
public class AlbumParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumParser"/> class.
/// </summary>
/// <param name="options">Naming options containing AlbumStackingPrefixes.</param>
public AlbumParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Function that determines if album is multipart.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if album is multipart.</returns>
public bool IsMultiPart(string path)
{
var filename = Path.GetFileName(path);

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -8,8 +5,17 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
/// <summary>
/// Static helper class to determine if file at path is audio file.
/// </summary>
public static class AudioFileParser
{
/// <summary>
/// Static helper method to determine if file at path is audio file.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions.</param>
/// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options)
{
var extension = Path.GetExtension(path);

View File

@ -7,6 +7,21 @@ namespace Emby.Naming.AudioBook
/// </summary>
public class AudioBookFileInfo : IComparable<AudioBookFileInfo>
{
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookFileInfo"/> class.
/// </summary>
/// <param name="path">Path to audiobook file.</param>
/// <param name="container">File type.</param>
/// <param name="partNumber">Number of part this file represents.</param>
/// <param name="chapterNumber">Number of chapter this file represents.</param>
public AudioBookFileInfo(string path, string container, int? partNumber = default, int? chapterNumber = default)
{
Path = path;
Container = container;
PartNumber = partNumber;
ChapterNumber = chapterNumber;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -31,14 +46,8 @@ namespace Emby.Naming.AudioBook
/// <value>The chapter number.</value>
public int? ChapterNumber { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is a directory.
/// </summary>
/// <value>The type.</value>
public bool IsDirectory { get; set; }
/// <inheritdoc />
public int CompareTo(AudioBookFileInfo other)
public int CompareTo(AudioBookFileInfo? other)
{
if (ReferenceEquals(this, other))
{

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@ -8,15 +5,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Parser class to extract part and/or chapter number from audiobook filename.
/// </summary>
public class AudioBookFilePathParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookFilePathParser"/> class.
/// </summary>
/// <param name="options">Naming options containing AudioBookPartsExpressions.</param>
public AudioBookFilePathParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Based on regex determines if filename includes part/chapter number.
/// </summary>
/// <param name="path">Path to audiobook file.</param>
/// <returns>Returns <see cref="AudioBookFilePathParser"/> object.</returns>
public AudioBookFilePathParserResult Parse(string path)
{
AudioBookFilePathParserResult result = default;
@ -52,8 +61,6 @@ namespace Emby.Naming.AudioBook
}
}
result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
return result;
}
}

View File

@ -1,14 +1,18 @@
#nullable enable
#pragma warning disable CS1591
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Data object for passing result of audiobook part/chapter extraction.
/// </summary>
public struct AudioBookFilePathParserResult
{
/// <summary>
/// Gets or sets optional number of path extracted from audiobook filename.
/// </summary>
public int? PartNumber { get; set; }
/// <summary>
/// Gets or sets optional number of chapter extracted from audiobook filename.
/// </summary>
public int? ChapterNumber { get; set; }
public bool Success { get; set; }
}
}

View File

@ -10,11 +10,18 @@ namespace Emby.Naming.AudioBook
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookInfo" /> class.
/// </summary>
public AudioBookInfo()
/// <param name="name">Name of audiobook.</param>
/// <param name="year">Year of audiobook release.</param>
/// <param name="files">List of files composing the actual audiobook.</param>
/// <param name="extras">List of extra files.</param>
/// <param name="alternateVersions">Alternative version of files.</param>
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
{
Files = new List<AudioBookFileInfo>();
Extras = new List<AudioBookFileInfo>();
AlternateVersions = new List<AudioBookFileInfo>();
Name = name;
Year = year;
Files = files ?? new List<AudioBookFileInfo>();
Extras = extras ?? new List<AudioBookFileInfo>();
AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
}
/// <summary>

View File

@ -1,6 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@ -8,40 +8,145 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Class used to resolve Name, Year, alternative files and extras from stack of files.
/// </summary>
public class AudioBookListResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
/// </summary>
/// <param name="options">Naming options passed along to <see cref="AudioBookResolver"/> and <see cref="AudioBookNameParser"/>.</param>
public AudioBookListResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves Name, Year and differentiate alternative files and extras from regular audiobook files.
/// </summary>
/// <param name="files">List of files related to audiobook.</param>
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
var audioBookResolver = new AudioBookResolver(_options);
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
.Select(i => audioBookResolver.Resolve(i.FullName, i.IsDirectory))
.Where(i => i != null)
.Select(i => audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var metadata = audiobookFileInfos
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(_options)
.ResolveAudioBooks(metadata);
.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList();
var stackFiles = stack.Files
.Select(i => audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();
stackFiles.Sort();
var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name };
var nameParserResult = new AudioBookNameParser(_options).Parse(stack.Name);
FindExtraAndAlternativeFiles(ref stackFiles, out var extras, out var alternativeVersions, nameParserResult);
var info = new AudioBookInfo(
nameParserResult.Name,
nameParserResult.Year,
stackFiles,
extras,
alternativeVersions);
yield return info;
}
}
private void FindExtraAndAlternativeFiles(ref List<AudioBookFileInfo> stackFiles, out List<AudioBookFileInfo> extras, out List<AudioBookFileInfo> alternativeVersions, AudioBookNameParserResult nameParserResult)
{
extras = new List<AudioBookFileInfo>();
alternativeVersions = new List<AudioBookFileInfo>();
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
foreach (var group in groupedBy)
{
if (group.Key.ChapterNumber == null && group.Key.PartNumber == null)
{
if (group.Count() > 1 || haveChaptersOrPages)
{
var ex = new List<AudioBookFileInfo>();
var alt = new List<AudioBookFileInfo>();
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
if (name.Equals("audiobook") ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
alt.Add(audioFile);
}
else
{
ex.Add(audioFile);
}
}
if (ex.Count > 0)
{
var extra = ex
.OrderBy(x => x.Container)
.ThenBy(x => x.Path)
.ToList();
stackFiles = stackFiles.Except(extra).ToList();
extras.AddRange(extra);
}
if (alt.Count > 0)
{
var alternatives = alt
.OrderBy(x => x.Container)
.ThenBy(x => x.Path)
.ToList();
var main = FindMainAudioBookFile(alternatives, nameParserResult.Name);
alternatives.Remove(main);
stackFiles = stackFiles.Except(alternatives).ToList();
alternativeVersions.AddRange(alternatives);
}
}
}
else if (group.Count() > 1)
{
var alternatives = group
.OrderBy(x => x.Container)
.ThenBy(x => x.Path)
.Skip(1)
.ToList();
stackFiles = stackFiles.Except(alternatives).ToList();
alternativeVersions.AddRange(alternatives);
}
}
}
private AudioBookFileInfo FindMainAudioBookFile(List<AudioBookFileInfo> files, string name)
{
var main = files.Find(x => Path.GetFileNameWithoutExtension(x.Path).Equals(name, StringComparison.OrdinalIgnoreCase));
main ??= files.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x.Path).Equals("audiobook", StringComparison.OrdinalIgnoreCase));
main ??= files.OrderBy(x => x.Container)
.ThenBy(x => x.Path)
.First();
return main;
}
}
}

View File

@ -0,0 +1,67 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Helper class to retrieve name and year from audiobook previously retrieved name.
/// </summary>
public class AudioBookNameParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookNameParser"/> class.
/// </summary>
/// <param name="options">Naming options containing AudioBookNamesExpressions.</param>
public AudioBookNameParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parse name and year from previously determined name of audiobook.
/// </summary>
/// <param name="name">Name of the audiobook.</param>
/// <returns>Returns <see cref="AudioBookNameParserResult"/> object.</returns>
public AudioBookNameParserResult Parse(string name)
{
AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions)
{
var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
if (match.Success)
{
if (result.Name == null)
{
var value = match.Groups["name"];
if (value.Success)
{
result.Name = value.Value;
}
}
if (!result.Year.HasValue)
{
var value = match.Groups["year"];
if (value.Success)
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.Year = intValue;
}
}
}
}
}
if (string.IsNullOrEmpty(result.Name))
{
result.Name = name;
}
return result;
}
}
}

View File

@ -0,0 +1,18 @@
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Data object used to pass result of name and year parsing.
/// </summary>
public struct AudioBookNameParserResult
{
/// <summary>
/// Gets or sets name of audiobook.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Year { get; set; }
}
}

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -7,35 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
/// <summary>
/// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
/// </summary>
public class AudioBookResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions and also used to pass to AudioBookFilePathParser.</param>
public AudioBookResolver(NamingOptions options)
{
_options = options;
}
public AudioBookFileInfo ParseFile(string path)
/// <summary>
/// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
/// </summary>
/// <param name="path">Path to audiobook file.</param>
/// <returns>Returns <see cref="AudioBookResolver"/> object.</returns>
public AudioBookFileInfo? Resolve(string path)
{
return Resolve(path, false);
}
public AudioBookFileInfo ParseDirectory(string path)
{
return Resolve(path, true);
}
public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
// TODO
if (isDirectory)
if (path.Length == 0 || Path.GetFileNameWithoutExtension(path).Length == 0)
{
// Return null to indicate this path will not be used, instead of stopping whole process with exception
return null;
}
@ -51,14 +46,11 @@ namespace Emby.Naming.AudioBook
var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
return new AudioBookFileInfo
{
Path = path,
Container = container,
ChapterNumber = parsingResult.ChapterNumber,
PartNumber = parsingResult.PartNumber,
IsDirectory = isDirectory
};
return new AudioBookFileInfo(
path,
container,
chapterNumber: parsingResult.ChapterNumber,
partNumber: parsingResult.PartNumber);
}
}
}

View File

@ -1,28 +1,32 @@
#pragma warning disable CS1591
using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Common
{
/// <summary>
/// Regular expressions for parsing TV Episodes.
/// </summary>
public class EpisodeExpression
{
private string _expression;
private Regex _regex;
private Regex? _regex;
public EpisodeExpression(string expression, bool byDate)
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeExpression"/> class.
/// </summary>
/// <param name="expression">Regular expressions.</param>
/// <param name="byDate">True if date is expected.</param>
public EpisodeExpression(string expression, bool byDate = false)
{
Expression = expression;
_expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty<string>();
SupportsAbsoluteEpisodeNumbers = true;
}
public EpisodeExpression(string expression)
: this(expression, false)
{
}
/// <summary>
/// Gets or sets raw expressions string.
/// </summary>
public string Expression
{
get => _expression;
@ -33,16 +37,34 @@ namespace Emby.Naming.Common
}
}
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if date can be find in expression.
/// </summary>
public bool IsByDate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression is optimistic.
/// </summary>
public bool IsOptimistic { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression is named.
/// </summary>
public bool IsNamed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets property indicating if expression supports episodes with absolute numbers.
/// </summary>
public bool SupportsAbsoluteEpisodeNumbers { get; set; }
/// <summary>
/// Gets or sets optional list of date formats used for date parsing.
/// </summary>
public string[] DateTimeFormats { get; set; }
/// <summary>
/// Gets a <see cref="Regex"/> expressions objects (creates it if null).
/// </summary>
public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.Common
{
/// <summary>
/// Type of audiovisual media.
/// </summary>
public enum MediaType
{
/// <summary>

View File

@ -1,15 +1,21 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
// ReSharper disable StringLiteralTypo
namespace Emby.Naming.Common
{
/// <summary>
/// Big ugly class containing lot of different naming options that should be split and injected instead of passes everywhere.
/// </summary>
public class NamingOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="NamingOptions"/> class.
/// </summary>
public NamingOptions()
{
VideoFileExtensions = new[]
@ -75,63 +81,52 @@ namespace Emby.Naming.Common
StubTypes = new[]
{
new StubTypeRule
{
StubType = "dvd",
Token = "dvd"
},
new StubTypeRule
{
StubType = "hddvd",
Token = "hddvd"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bluray"
},
new StubTypeRule
{
StubType = "bluray",
Token = "brrip"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bd25"
},
new StubTypeRule
{
StubType = "bluray",
Token = "bd50"
},
new StubTypeRule
{
StubType = "vhs",
Token = "vhs"
},
new StubTypeRule
{
StubType = "tv",
Token = "HDTV"
},
new StubTypeRule
{
StubType = "tv",
Token = "PDTV"
},
new StubTypeRule
{
StubType = "tv",
Token = "DSR"
}
new StubTypeRule(
stubType: "dvd",
token: "dvd"),
new StubTypeRule(
stubType: "hddvd",
token: "hddvd"),
new StubTypeRule(
stubType: "bluray",
token: "bluray"),
new StubTypeRule(
stubType: "bluray",
token: "brrip"),
new StubTypeRule(
stubType: "bluray",
token: "bd25"),
new StubTypeRule(
stubType: "bluray",
token: "bd50"),
new StubTypeRule(
stubType: "vhs",
token: "vhs"),
new StubTypeRule(
stubType: "tv",
token: "HDTV"),
new StubTypeRule(
stubType: "tv",
token: "PDTV"),
new StubTypeRule(
stubType: "tv",
token: "DSR")
};
VideoFileStackingExpressions = new[]
{
"(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$",
"(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$",
"(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$"
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
};
CleanDateTimes = new[]
@ -142,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@ -255,7 +250,7 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@ -264,7 +259,7 @@ namespace Emby.Naming.Common
"yyyy_MM_dd"
}
},
new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@ -286,7 +281,12 @@ namespace Emby.Naming.Common
{
SupportsAbsoluteEpisodeNumbers = true
},
new EpisodeExpression(@"[\\\\/\\._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$")
// Case Closed (1996-2007)/Case Closed - 317.mkv
// /server/anything_102.mp4
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
// /server/anything_1996.11.14.mp4
new EpisodeExpression(@"[\\/._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/_])*)[\\\/._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\.[1-9])(?![0-9]))?)([._ -][^\\\/]*)$")
{
IsOptimistic = true,
IsNamed = true,
@ -381,247 +381,193 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Filename,
Token = "trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "-trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = ".trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = "_trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Trailer,
RuleType = ExtraRuleType.Suffix,
Token = " trailer",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Filename,
Token = "sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "-sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = ".sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = "_sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.Suffix,
Token = " sample",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.ThemeSong,
RuleType = ExtraRuleType.Filename,
Token = "theme",
MediaType = MediaType.Audio
},
new ExtraRule
{
ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.Suffix,
Token = "-scene",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-clip",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.Suffix,
Token = "-interview",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.Suffix,
Token = "-behindthescenes",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.Suffix,
Token = "-deleted",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-featurette",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.Suffix,
Token = "-short",
MediaType = MediaType.Video
},
new ExtraRule
{
ExtraType = ExtraType.BehindTheScenes,
RuleType = ExtraRuleType.DirectoryName,
Token = "behind the scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.DeletedScene,
RuleType = ExtraRuleType.DirectoryName,
Token = "deleted scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Interview,
RuleType = ExtraRuleType.DirectoryName,
Token = "interviews",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Scene,
RuleType = ExtraRuleType.DirectoryName,
Token = "scenes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Sample,
RuleType = ExtraRuleType.DirectoryName,
Token = "samples",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "shorts",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Clip,
RuleType = ExtraRuleType.DirectoryName,
Token = "featurettes",
MediaType = MediaType.Video,
},
new ExtraRule
{
ExtraType = ExtraType.Unknown,
RuleType = ExtraRuleType.DirectoryName,
Token = "extras",
MediaType = MediaType.Video,
},
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"-trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
".trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"_trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
" trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
"-sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
".sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
"_sample",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
" sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-clip",
MediaType.Video),
new ExtraRule(
ExtraType.Interview,
ExtraRuleType.Suffix,
"-interview",
MediaType.Video),
new ExtraRule(
ExtraType.BehindTheScenes,
ExtraRuleType.Suffix,
"-behindthescenes",
MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.Suffix,
"-deleted",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-featurette",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-short",
MediaType.Video),
new ExtraRule(
ExtraType.BehindTheScenes,
ExtraRuleType.DirectoryName,
"behind the scenes",
MediaType.Video),
new ExtraRule(
ExtraType.DeletedScene,
ExtraRuleType.DirectoryName,
"deleted scenes",
MediaType.Video),
new ExtraRule(
ExtraType.Interview,
ExtraRuleType.DirectoryName,
"interviews",
MediaType.Video),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.DirectoryName,
"scenes",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.DirectoryName,
"samples",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.DirectoryName,
"shorts",
MediaType.Video),
new ExtraRule(
ExtraType.Clip,
ExtraRuleType.DirectoryName,
"featurettes",
MediaType.Video),
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
MediaType.Video),
};
Format3DRules = new[]
{
// Kodi rules:
new Format3DRule
{
PreceedingToken = "3d",
Token = "hsbs"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "sbs"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "htab"
},
new Format3DRule
{
PreceedingToken = "3d",
Token = "tab"
},
// Media Browser rules:
new Format3DRule
{
Token = "fsbs"
},
new Format3DRule
{
Token = "hsbs"
},
new Format3DRule
{
Token = "sbs"
},
new Format3DRule
{
Token = "ftab"
},
new Format3DRule
{
Token = "htab"
},
new Format3DRule
{
Token = "tab"
},
new Format3DRule
{
Token = "sbs3d"
},
new Format3DRule
{
Token = "mvc"
}
new Format3DRule(
precedingToken: "3d",
token: "hsbs"),
new Format3DRule(
precedingToken: "3d",
token: "sbs"),
new Format3DRule(
precedingToken: "3d",
token: "htab"),
new Format3DRule(
precedingToken: "3d",
token: "tab"),
// Media Browser rules:
new Format3DRule("fsbs"),
new Format3DRule("hsbs"),
new Format3DRule("sbs"),
new Format3DRule("ftab"),
new Format3DRule("htab"),
new Format3DRule("tab"),
new Format3DRule("sbs3d"),
new Format3DRule("mvc")
};
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
@ -631,13 +577,20 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
"(?<part>[0-9]+)$",
@"(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
AudioBookNamesExpressions = new[]
{
// Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$"
};
var extensions = VideoFileExtensions.ToList();
extensions.AddRange(new[]
@ -673,7 +626,7 @@ namespace Emby.Naming.Common
".mxf"
});
MultipleEpisodeExpressions = new string[]
MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@ -697,56 +650,139 @@ namespace Emby.Naming.Common
Compile();
}
/// <summary>
/// Gets or sets list of audio file extensions.
/// </summary>
public string[] AudioFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of album stacking prefixes.
/// </summary>
public string[] AlbumStackingPrefixes { get; set; }
/// <summary>
/// Gets or sets list of subtitle file extensions.
/// </summary>
public string[] SubtitleFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of subtitles flag delimiters.
/// </summary>
public char[] SubtitleFlagDelimiters { get; set; }
/// <summary>
/// Gets or sets list of subtitle forced flags.
/// </summary>
public string[] SubtitleForcedFlags { get; set; }
/// <summary>
/// Gets or sets list of subtitle default flags.
/// </summary>
public string[] SubtitleDefaultFlags { get; set; }
/// <summary>
/// Gets or sets list of episode regular expressions.
/// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw episode without season regular expressions strings.
/// </summary>
public string[] EpisodeWithoutSeasonExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw multi-part episodes regular expressions strings.
/// </summary>
public string[] EpisodeMultiPartExpressions { get; set; }
/// <summary>
/// Gets or sets list of video file extensions.
/// </summary>
public string[] VideoFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of video stub file extensions.
/// </summary>
public string[] StubFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of raw audiobook parts regular expressions strings.
/// </summary>
public string[] AudioBookPartsExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw audiobook names regular expressions strings.
/// </summary>
public string[] AudioBookNamesExpressions { get; set; }
/// <summary>
/// Gets or sets list of stub type rules.
/// </summary>
public StubTypeRule[] StubTypes { get; set; }
/// <summary>
/// Gets or sets list of video flag delimiters.
/// </summary>
public char[] VideoFlagDelimiters { get; set; }
/// <summary>
/// Gets or sets list of 3D Format rules.
/// </summary>
public Format3DRule[] Format3DRules { get; set; }
/// <summary>
/// Gets or sets list of raw video file-stacking expressions strings.
/// </summary>
public string[] VideoFileStackingExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings.
/// </summary>
public string[] CleanDateTimes { get; set; }
/// <summary>
/// Gets or sets list of raw clean strings regular expressions strings.
/// </summary>
public string[] CleanStrings { get; set; }
/// <summary>
/// Gets or sets list of multi-episode regular expressions.
/// </summary>
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of extra rules for videos.
/// </summary>
public ExtraRule[] VideoExtraRules { get; set; }
public Regex[] VideoFileStackingRegexes { get; private set; }
/// <summary>
/// Gets list of video file-stack regular expressions.
/// </summary>
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanDateTimeRegexes { get; private set; }
/// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] CleanStringRegexes { get; private set; }
/// <summary>
/// Gets list of clean string regular expressions.
/// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; }
/// <summary>
/// Gets list of episode without season regular expressions.
/// </summary>
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
public Regex[] EpisodeMultiPartRegexes { get; private set; }
/// <summary>
/// Gets list of multi-part episode regular expressions.
/// </summary>
public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Compiles raw regex strings into regexes.
/// </summary>
public void Compile()
{
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -14,6 +14,7 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@ -38,7 +39,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->

View File

@ -1,9 +1,23 @@
#pragma warning disable CS1591
namespace Emby.Naming.Subtitles
{
/// <summary>
/// Class holding information about subtitle.
/// </summary>
public class SubtitleInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="isDefault">Is subtitle default.</param>
/// <param name="isForced">Is subtitle forced.</param>
public SubtitleInfo(string path, bool isDefault, bool isForced)
{
Path = path;
IsDefault = isDefault;
IsForced = isForced;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -14,7 +28,7 @@ namespace Emby.Naming.Subtitles
/// Gets or sets the language.
/// </summary>
/// <value>The language.</value>
public string Language { get; set; }
public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is default.

View File

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -8,20 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.Subtitles
{
/// <summary>
/// Subtitle Parser class.
/// </summary>
public class SubtitleParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
public SubtitleParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
public SubtitleInfo? ParseFile(string path)
{
if (path.Length == 0)
{
throw new ArgumentException("File path can't be empty.", nameof(path));
return null;
}
var extension = Path.GetExtension(path);
@ -31,12 +40,10 @@ namespace Emby.Naming.Subtitles
}
var flags = GetFlags(path);
var info = new SubtitleInfo
{
Path = path,
IsDefault = _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))
};
var info = new SubtitleInfo(
path,
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))

View File

@ -1,9 +1,19 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for Episode information.
/// </summary>
public class EpisodeInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeInfo"/> class.
/// </summary>
/// <param name="path">Path to the file.</param>
public EpisodeInfo(string path)
{
Path = path;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -14,19 +24,19 @@ namespace Emby.Naming.TV
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
public string Container { get; set; }
public string? Container { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string SeriesName { get; set; }
public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@ -44,20 +54,41 @@ namespace Emby.Naming.TV
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
public string? StubType { get; set; }
/// <summary>
/// Gets or sets optional season number.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// Gets or sets optional episode number.
/// </summary>
public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; }
/// <summary>
/// Gets or sets optional ending episode number. For multi-episode files 1-13.
/// </summary>
public int? EndingEpisodeNumber { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Month { get; set; }
/// <summary>
/// Gets or sets optional day of release.
/// </summary>
public int? Day { get; set; }
/// <summary>
/// Gets or sets a value indicating whether by date expression was used.
/// </summary>
public bool IsByDate { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
@ -9,15 +6,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to parse information about episode from path.
/// </summary>
public class EpisodePathParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodePathParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
public EpisodePathParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parses information about episode from path.
/// </summary>
/// <param name="path">Path.</param>
/// <param name="isDirectory">Is path for a directory or file.</param>
/// <param name="isNamed">Do we want to use IsNamed expressions.</param>
/// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
/// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
/// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
/// <returns>Returns <see cref="EpisodePathParserResult"/> object.</returns>
public EpisodePathParserResult Parse(
string path,
bool isDirectory,
@ -146,7 +160,7 @@ namespace Emby.Naming.TV
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpsiodeNumber = num;
result.EndingEpisodeNumber = num;
}
}
}
@ -186,7 +200,7 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info)
{
var expressions = _options.MultipleEpisodeExpressions.ToList();
var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList();
if (string.IsNullOrEmpty(info.SeriesName))
{
@ -200,11 +214,6 @@ namespace Emby.Naming.TV
{
foreach (var i in expressions)
{
if (!i.IsNamed)
{
continue;
}
var result = Parse(path, i);
if (!result.Success)
@ -217,13 +226,13 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue)
if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
{
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
if (!string.IsNullOrEmpty(info.SeriesName)
&& (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
&& (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue))
{
break;
}

View File

@ -1,25 +1,54 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for <see cref="EpisodePathParser"/> result.
/// </summary>
public class EpisodePathParserResult
{
/// <summary>
/// Gets or sets optional season number.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
/// Gets or sets optional episode number.
/// </summary>
public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; }
/// <summary>
/// Gets or sets optional ending episode number. For multi-episode files 1-13.
/// </summary>
public int? EndingEpisodeNumber { get; set; }
public string SeriesName { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether parsing was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets a value indicating whether by date expression was used.
/// </summary>
public bool IsByDate { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets optional year of release.
/// </summary>
public int? Month { get; set; }
/// <summary>
/// Gets or sets optional day of release.
/// </summary>
public int? Day { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -9,15 +6,32 @@ using Emby.Naming.Video;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to resolve information about episode from path.
/// </summary>
public class EpisodeResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolve information about episode from path.
/// </summary>
/// <param name="path">Path.</param>
/// <param name="isDirectory">Is path for a directory or file.</param>
/// <param name="isNamed">Do we want to use IsNamed expressions.</param>
/// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
/// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
/// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
/// <returns>Returns null or <see cref="EpisodeInfo"/> object if successful.</returns>
public EpisodeInfo? Resolve(
string path,
bool isDirectory,
@ -54,12 +68,11 @@ namespace Emby.Naming.TV
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
return new EpisodeInfo
return new EpisodeInfo(path)
{
Path = path,
Container = container,
IsStub = isStub,
EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber,
EndingEpisodeNumber = parsingResult.EndingEpisodeNumber,
EpisodeNumber = parsingResult.EpisodeNumber,
SeasonNumber = parsingResult.SeasonNumber,
SeriesName = parsingResult.SeriesName,

View File

@ -1,11 +1,12 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
namespace Emby.Naming.TV
{
/// <summary>
/// Class to parse season paths.
/// </summary>
public static class SeasonPathParser
{
/// <summary>
@ -23,6 +24,13 @@ namespace Emby.Naming.TV
"stagione"
};
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
/// <param name="path">Path to season.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
@ -101,9 +109,9 @@ namespace Emby.Naming.TV
}
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < parts.Length; i++)
foreach (var part in parts)
{
if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
{
return (seasonNumber, true);
}
@ -139,7 +147,7 @@ namespace Emby.Naming.TV
var numericStart = -1;
var length = 0;
var hasOpenParenth = false;
var hasOpenParenthesis = false;
var isSeasonFolder = true;
// Find out where the numbers start, and then keep going until they end
@ -147,7 +155,7 @@ namespace Emby.Naming.TV
{
if (char.IsNumber(path[i]))
{
if (!hasOpenParenth)
if (!hasOpenParenthesis)
{
if (numericStart == -1)
{
@ -167,11 +175,11 @@ namespace Emby.Naming.TV
var currentChar = path[i];
if (currentChar == '(')
{
hasOpenParenth = true;
hasOpenParenthesis = true;
}
else if (currentChar == ')')
{
hasOpenParenth = false;
hasOpenParenthesis = false;
}
}

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.TV
{
/// <summary>
/// Data object to pass result of <see cref="SeasonPathParser"/>.
/// </summary>
public class SeasonPathParserResult
{
/// <summary>
@ -16,6 +17,10 @@ namespace Emby.Naming.TV
/// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
public bool Success { get; set; }
/// <summary>
/// Gets or sets a value indicating whether "Is season folder".
/// Seems redundant and barely used.
/// </summary>
public bool IsSeasonFolder { get; set; }
}
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
@ -12,9 +9,20 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanDateTimeParser
{
/// <summary>
/// Attempts to clean the name.
/// </summary>
/// <param name="name">Name of video.</param>
/// <param name="cleanDateTimeRegexes">Optional list of regexes to clean the name.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> object.</returns>
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{
CleanDateTimeResult result = new CleanDateTimeResult(name);
if (string.IsNullOrEmpty(name))
{
return result;
}
var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++)
{

View File

@ -1,22 +1,21 @@
#pragma warning disable CS1591
#nullable enable
namespace Emby.Naming.Video
{
/// <summary>
/// Holder structure for name and year.
/// </summary>
public readonly struct CleanDateTimeResult
{
public CleanDateTimeResult(string name, int? year)
/// <summary>
/// Initializes a new instance of the <see cref="CleanDateTimeResult"/> struct.
/// </summary>
/// <param name="name">Name of video.</param>
/// <param name="year">Year of release.</param>
public CleanDateTimeResult(string name, int? year = null)
{
Name = name;
Year = year;
}
public CleanDateTimeResult(string name)
{
Name = name;
Year = null;
}
/// <summary>
/// Gets the name.
/// </summary>

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@ -12,6 +9,13 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanStringParser
{
/// <summary>
/// Attempts to extract clean name with regular expressions.
/// </summary>
/// <param name="name">Name of file.</param>
/// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns>
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
var len = expressions.Count;

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
@ -9,15 +7,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
public class ExtraResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
public ExtraResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path)
{
return _options.VideoExtraRules
@ -43,10 +53,6 @@ namespace Emby.Naming.Video
return result;
}
}
else
{
return result;
}
if (rule.RuleType == ExtraRuleType.Filename)
{

View File

@ -1,9 +1,10 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
/// <summary>
/// Holder object for passing results from ExtraResolver.
/// </summary>
public class ExtraResult
{
/// <summary>
@ -16,6 +17,6 @@ namespace Emby.Naming.Video
/// Gets or sets the rule.
/// </summary>
/// <value>The rule.</value>
public ExtraRule Rule { get; set; }
public ExtraRule? Rule { get; set; }
}
}

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
using MediaType = Emby.Naming.Common.MediaType;
@ -10,6 +8,21 @@ namespace Emby.Naming.Video
/// </summary>
public class ExtraRule
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtraRule"/> class.
/// </summary>
/// <param name="extraType">Type of extra.</param>
/// <param name="ruleType">Type of rule.</param>
/// <param name="token">Token.</param>
/// <param name="mediaType">Media type.</param>
public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType)
{
Token = token;
ExtraType = extraType;
RuleType = ruleType;
MediaType = mediaType;
}
/// <summary>
/// Gets or sets the token to use for matching against the file path.
/// </summary>

View File

@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Extra rules type to determine against what <see cref="ExtraRule.Token"/> should be matched.
/// </summary>
public enum ExtraRuleType
{
/// <summary>
@ -22,6 +23,6 @@ namespace Emby.Naming.Video
/// <summary>
/// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file.
/// </summary>
DirectoryName = 3,
DirectoryName = 3
}
}

View File

@ -1,24 +1,43 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
namespace Emby.Naming.Video
{
/// <summary>
/// Object holding list of files paths with additional information.
/// </summary>
public class FileStack
{
/// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary>
public FileStack()
{
Files = new List<string>();
}
public string Name { get; set; }
/// <summary>
/// Gets or sets name of file stack.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets list of paths in stack.
/// </summary>
public List<string> Files { get; set; }
/// <summary>
/// Gets or sets a value indicating whether stack is directory stack.
/// </summary>
public bool IsDirectoryStack { get; set; }
/// <summary>
/// Helper function to determine if path is in the stack.
/// </summary>
/// <param name="file">Path of desired file.</param>
/// <param name="isDirectory">Requested type of stack.</param>
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
if (IsDirectoryStack == isDirectory)

View File

@ -1,37 +1,53 @@
#pragma warning disable CS1591
using System;
using System.IO;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parses list of flags from filename based on delimiters.
/// </summary>
public class FlagParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="FlagParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
public FlagParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path)
{
return GetFlags(path, _options.VideoFlagDelimiters);
}
public string[] GetFlags(string path, char[] delimeters)
/// <summary>
/// Parses flags from filename based on delimiters.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="delimiters">Delimiters used to extract flags.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path, char[] delimiters)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
return Array.Empty<string>();
}
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries);
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
}
}
}

View File

@ -1,28 +1,38 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parste 3D format related flags.
/// </summary>
public class Format3DParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
public Format3DParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
public Format3DResult Parse(string path)
{
int oldLen = _options.VideoFlagDelimiters.Length;
var delimeters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimeters, 0);
delimeters[oldLen] = ' ';
var delimiters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
delimiters[oldLen] = ' ';
return Parse(new FlagParser(_options).GetFlags(path, delimeters));
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
}
internal Format3DResult Parse(string[] videoFlags)
@ -44,7 +54,7 @@ namespace Emby.Naming.Video
{
var result = new Format3DResult();
if (string.IsNullOrEmpty(rule.PreceedingToken))
if (string.IsNullOrEmpty(rule.PrecedingToken))
{
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
@ -57,13 +67,13 @@ namespace Emby.Naming.Video
else
{
var foundPrefix = false;
string format = null;
string? format = null;
foreach (var flag in videoFlags)
{
if (foundPrefix)
{
result.Tokens.Add(rule.PreceedingToken);
result.Tokens.Add(rule.PrecedingToken);
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
{
@ -74,7 +84,7 @@ namespace Emby.Naming.Video
break;
}
foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
}
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);

View File

@ -1,11 +1,15 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace Emby.Naming.Video
{
/// <summary>
/// Helper object to return data from <see cref="Format3DParser"/>.
/// </summary>
public class Format3DResult
{
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary>
public Format3DResult()
{
Tokens = new List<string>();
@ -21,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets the tokens.

View File

@ -1,9 +1,21 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Data holder class for 3D format rule.
/// </summary>
public class Format3DRule
{
/// <summary>
/// Initializes a new instance of the <see cref="Format3DRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <param name="precedingToken">Token present before current token.</param>
public Format3DRule(string token, string? precedingToken = null)
{
Token = token;
PrecedingToken = precedingToken;
}
/// <summary>
/// Gets or sets the token.
/// </summary>
@ -11,9 +23,9 @@ namespace Emby.Naming.Video
public string Token { get; set; }
/// <summary>
/// Gets or sets the preceeding token.
/// Gets or sets the preceding token.
/// </summary>
/// <value>The preceeding token.</value>
public string PreceedingToken { get; set; }
/// <value>The preceding token.</value>
public string? PrecedingToken { get; set; }
}
}

View File

@ -1,58 +1,88 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
public class StackResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="StackResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
public StackResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
}
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files)
/// <summary>
/// Resolves audiobooks from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file =>
file.IsDirectory
? file.FullName
: Path.GetDirectoryName(file.FullName));
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
foreach (var directory in groupedDirectoryFiles)
{
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
foreach (var file in directory)
if (string.IsNullOrEmpty(directory.Key))
{
if (file.IsDirectory)
foreach (var file in directory)
{
continue;
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
stack.Files.Add(file.Path);
yield return stack;
}
}
else
{
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
foreach (var file in directory)
{
stack.Files.Add(file.Path);
}
stack.Files.Add(file.FullName);
yield return stack;
}
yield return stack;
}
}
/// <summary>
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var resolver = new VideoResolver(_options);
@ -81,10 +111,10 @@ namespace Emby.Naming.Video
if (match1.Success)
{
var title1 = match1.Groups[1].Value;
var volume1 = match1.Groups[2].Value;
var ignore1 = match1.Groups[3].Value;
var extension1 = match1.Groups[4].Value;
var title1 = match1.Groups["title"].Value;
var volume1 = match1.Groups["volume"].Value;
var ignore1 = match1.Groups["ignore"].Value;
var extension1 = match1.Groups["extension"].Value;
var j = i + 1;
while (j < list.Count)

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -8,13 +5,23 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolve if file is stub (.disc).
/// </summary>
public static class StubResolver
{
/// <summary>
/// Tries to resolve if file is stub (.disc).
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="options">NamingOptions containing StubFileExtensions and StubTypes.</param>
/// <param name="stubType">Stub type.</param>
/// <returns>True if file is a stub.</returns>
public static bool TryResolveFile(string path, NamingOptions options, out string? stubType)
{
stubType = default;
if (path == null)
if (string.IsNullOrEmpty(path))
{
return false;
}

View File

@ -1,19 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
public struct StubResult
{
/// <summary>
/// Gets or sets a value indicating whether this instance is stub.
/// </summary>
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
public bool IsStub { get; set; }
/// <summary>
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
}
}

View File

@ -1,9 +1,21 @@
#pragma warning disable CS1591
namespace Emby.Naming.Video
{
/// <summary>
/// Data class holding information about Stub type rule.
/// </summary>
public class StubTypeRule
{
/// <summary>
/// Initializes a new instance of the <see cref="StubTypeRule"/> class.
/// </summary>
/// <param name="token">Token.</param>
/// <param name="stubType">Stub type.</param>
public StubTypeRule(string token, string stubType)
{
Token = token;
StubType = stubType;
}
/// <summary>
/// Gets or sets the token.
/// </summary>

View File

@ -7,6 +7,35 @@ namespace Emby.Naming.Video
/// </summary>
public class VideoFileInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="VideoFileInfo"/> class.
/// </summary>
/// <param name="name">Name of file.</param>
/// <param name="path">Path to the file.</param>
/// <param name="container">Container type.</param>
/// <param name="year">Year of release.</param>
/// <param name="extraType">Extra type.</param>
/// <param name="extraRule">Extra rule.</param>
/// <param name="format3D">Format 3D.</param>
/// <param name="is3D">Is 3D.</param>
/// <param name="isStub">Is Stub.</param>
/// <param name="stubType">Stub type.</param>
/// <param name="isDirectory">Is directory.</param>
public VideoFileInfo(string name, string path, string? container, int? year = default, ExtraType? extraType = default, ExtraRule? extraRule = default, string? format3D = default, bool is3D = default, bool isStub = default, string? stubType = default, bool isDirectory = default)
{
Path = path;
Container = container;
Name = name;
Year = year;
ExtraType = extraType;
ExtraRule = extraRule;
Format3D = format3D;
Is3D = is3D;
IsStub = isStub;
StubType = stubType;
IsDirectory = isDirectory;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
@ -17,7 +46,7 @@ namespace Emby.Naming.Video
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
public string Container { get; set; }
public string? Container { get; set; }
/// <summary>
/// Gets or sets the name.
@ -41,13 +70,13 @@ namespace Emby.Naming.Video
/// Gets or sets the extra rule.
/// </summary>
/// <value>The extra rule.</value>
public ExtraRule ExtraRule { get; set; }
public ExtraRule? ExtraRule { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string Format3D { get; set; }
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@ -65,7 +94,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
public string StubType { get; set; }
public string? StubType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is a directory.
@ -84,8 +113,7 @@ namespace Emby.Naming.Video
/// <inheritdoc />
public override string ToString()
{
// Makes debugging easier
return Name ?? base.ToString();
return "VideoFileInfo(Name: '" + Name + "')";
}
}
}

View File

@ -12,7 +12,7 @@ namespace Emby.Naming.Video
/// Initializes a new instance of the <see cref="VideoInfo" /> class.
/// </summary>
/// <param name="name">The name.</param>
public VideoInfo(string name)
public VideoInfo(string? name)
{
Name = name;
@ -25,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// Gets or sets the year.

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
@ -11,22 +9,35 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
public class VideoListResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
public VideoListResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files togeather when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
.Where(i => i != null)
.OfType<VideoFileInfo>()
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
@ -39,7 +50,7 @@ namespace Emby.Naming.Video
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
.Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List<VideoInfo>();
@ -48,7 +59,9 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList()
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
@ -133,7 +146,7 @@ namespace Emby.Naming.Video
}
// If there's only one video, accept all trailers
// Be lenient because people use all kinds of mish mash conventions with trailers
// Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
@ -203,15 +216,21 @@ namespace Emby.Naming.Video
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
private bool IsEligibleForMultiVersion(string folderName, string testFilename)
private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
{
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.ToString();
}
testFilename = testFilename.Substring(folderName.Length).Trim();
return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-'
|| testFilename[0].Equals('-')
|| testFilename[0].Equals('_')
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
}

View File

@ -1,6 +1,3 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -8,10 +5,18 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </summary>
public class VideoResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
public VideoResolver(NamingOptions options)
{
_options = options;
@ -22,7 +27,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveDirectory(string path)
public VideoFileInfo? ResolveDirectory(string? path)
{
return Resolve(path, true);
}
@ -32,7 +37,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveFile(string path)
public VideoFileInfo? ResolveFile(string? path)
{
return Resolve(path, false);
}
@ -45,11 +50,11 @@ namespace Emby.Naming.Video
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true)
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
return null;
}
bool isStub = false;
@ -99,39 +104,58 @@ namespace Emby.Naming.Video
}
}
return new VideoFileInfo
{
Path = path,
Container = container,
IsStub = isStub,
Name = name,
Year = year,
StubType = stubType,
Is3D = format3DResult.Is3D,
Format3D = format3DResult.Format3D,
ExtraType = extraResult.ExtraType,
IsDirectory = isDirectory,
ExtraRule = extraResult.Rule
};
return new VideoFileInfo(
path: path,
container: container,
isStub: isStub,
name: name,
year: year,
stubType: stubType,
is3D: format3DResult.Is3D,
format3D: format3DResult.Format3D,
extraType: extraResult.ExtraType,
isDirectory: isDirectory,
extraRule: extraResult.Rule);
}
/// <summary>
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if is video file.</returns>
public bool IsVideoFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>True if is video file stub.</returns>
public bool IsStubFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
public CleanDateTimeResult CleanDateTime(string name)
{
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);

View File

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

View File

@ -83,7 +83,7 @@ namespace Emby.Notifications
return Task.CompletedTask;
}
private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
{
var type = NotificationType.ServerRestartRequired.ToString();
@ -99,7 +99,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
var entry = e.Argument;
@ -132,7 +132,7 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
{
if (!_appHost.HasUpdateAvailable)
{
@ -151,7 +151,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@ -197,7 +197,7 @@ namespace Emby.Notifications
return item.SourceType == SourceType.Library;
}
private async void LibraryUpdateTimerCallback(object state)
private async void LibraryUpdateTimerCallback(object? state)
{
List<BaseItem> items;
@ -209,7 +209,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null;
}
items = items.Take(10).ToList();
if (items.Count > 10)
{
items = items.GetRange(0, 10);
}
foreach (var item in items)
{

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
configuration = Activator.CreateInstance(type);
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Mvc;
@ -127,8 +126,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory;
private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
/// <summary>
@ -258,8 +255,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
_jsonSerializer = new JsonSerializer();
_jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@ -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,21 +812,6 @@ namespace Emby.Server.Implementations
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
private IPlugin LoadPlugin(IPlugin plugin)
{
try
{
plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
return null;
}
return plugin;
}
/// <summary>
/// Discovers the types.
/// </summary>
@ -840,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)
@ -994,80 +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
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
{
// Versioned folder.
versions.Add((ver, metafile, dir));
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
}
// 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
@ -1077,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;
}
@ -1092,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);
@ -1104,7 +1083,7 @@ namespace Emby.Server.Implementations
}
}
return dllList;
return versions;
}
/// <summary>
@ -1115,21 +1094,24 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
foreach (var file in plugin.DllFiles)
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
}
@ -1396,7 +1378,7 @@ namespace Emby.Server.Implementations
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Common.Cryptography.Constants;
@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
}
using var h = HashAlgorithm.Create(hashMethod);
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
if (salt.Length == 0)
{
return h.ComputeHash(bytes);

View File

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

View File

@ -107,20 +107,6 @@ namespace Emby.Server.Implementations.Data
return null;
}
public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
{
var commandText = string.Format(
CultureInfo.InvariantCulture,
"attach @path as {0};",
alias);
using (var statement = db.PrepareStatement(commandText))
{
statement.TryBind("@path", path);
statement.MoveNext();
}
}
public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
{
return result[index].SQLiteType == SQLiteType.Null;

View File

@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{
connection.RunQueries(queries);
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
SaveItemsInTranscation(db, tuples);
}, TransactionMode);
@ -1004,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
return;
}
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
@ -1054,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
return;
}
var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
var list = new List<ItemImageInfo>();
foreach (var part in parts)
{
@ -1093,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
public ItemImageInfo ItemImageInfoFromValueString(string value)
{
var parts = value.Split(new[] { '*' }, StringSplitOptions.None);
var parts = value.Split('*', StringSplitOptions.None);
if (parts.Length < 3)
{
@ -1529,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
{
if (!reader.IsDBNull(index))
{
item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
@ -1590,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
{
IEnumerable<MetadataField> GetLockedFields(string s)
{
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
{
if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
@ -1609,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
{
if (!reader.IsDBNull(index))
{
item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
@ -1619,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
{
if (!reader.IsDBNull(index))
{
item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
@ -1633,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
{
IEnumerable<TrailerType> GetTrailerTypes(string s)
{
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
{
if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
@ -1808,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
{
if (!reader.IsDBNull(index))
{
item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
}
index++;
@ -1845,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
{
if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
{
hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
{
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
// First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@ -2263,7 +2267,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
@ -2400,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
if (string.IsNullOrEmpty(item.OfficialRating))
{
builder.Append("((OfficialRating is null) * 10)");
builder.Append("(OfficialRating is null * 10)");
}
else
{
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
}
if (item.ProductionYear.HasValue)
@ -2413,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
}
//// genres, tags
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
// genres, tags, studios, person, year?
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
if (item is MusicArtist)
{
// Match albums where the artist is AlbumArtist against other albums.
// It is assumed that similar albums => similar artists.
builder.Append(
@"+ (WITH artistValues AS (
SELECT DISTINCT albumValues.CleanValue
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
), similarArtist AS (
SELECT albumValues.ItemId
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
}
builder.Append(") as SimilarityScore");
@ -2922,7 +2943,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
@ -3291,7 +3313,6 @@ namespace Emby.Server.Implementations.Data
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
@ -3326,7 +3347,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<Guid>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
@ -3910,7 +3932,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase))
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{
if (query.IsPlayed.Value)
{
@ -4751,29 +4773,29 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<string>();
if (IsTypeInQuery(typeof(Person).Name, query))
if (IsTypeInQuery(nameof(Person), query))
{
list.Add(typeof(Person).Name);
list.Add(nameof(Person));
}
if (IsTypeInQuery(typeof(Genre).Name, query))
if (IsTypeInQuery(nameof(Genre), query))
{
list.Add(typeof(Genre).Name);
list.Add(nameof(Genre));
}
if (IsTypeInQuery(typeof(MusicGenre).Name, query))
if (IsTypeInQuery(nameof(MusicGenre), query))
{
list.Add(typeof(MusicGenre).Name);
list.Add(nameof(MusicGenre));
}
if (IsTypeInQuery(typeof(MusicArtist).Name, query))
if (IsTypeInQuery(nameof(MusicArtist), query))
{
list.Add(typeof(MusicArtist).Name);
list.Add(nameof(MusicArtist));
}
if (IsTypeInQuery(typeof(Studio).Name, query))
if (IsTypeInQuery(nameof(Studio), query))
{
list.Add(typeof(Studio).Name);
list.Add(nameof(Studio));
}
return list;
@ -4828,12 +4850,12 @@ namespace Emby.Server.Implementations.Data
var types = new[]
{
typeof(Episode).Name,
typeof(Video).Name,
typeof(Movie).Name,
typeof(MusicVideo).Name,
typeof(Series).Name,
typeof(Season).Name
nameof(Episode),
nameof(Video),
nameof(Movie),
nameof(MusicVideo),
nameof(Series),
nameof(Season)
};
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@ -4901,7 +4923,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
connection.ExecuteAll(sql);
}, TransactionMode);
@ -4952,7 +4975,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var idBlob = id.ToByteArray();
@ -4996,26 +5020,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
var commandText = "select Distinct Name from People";
var commandText = new StringBuilder("select Distinct p.Name from People p");
if (query.User != null && query.IsFavorite.HasValue)
{
commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
commandText.Append(typeof(Person).FullName);
commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
}
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
commandText += " where " + string.Join(" AND ", whereClauses);
commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
}
commandText += " order by ListOrder";
commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
commandText += " LIMIT " + query.Limit;
commandText.Append(" LIMIT ").Append(query.Limit);
}
using (var connection = GetConnection(true))
{
var list = new List<string>();
using (var statement = PrepareStatement(connection, commandText))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@ -5039,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People";
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
var whereClauses = GetPeopleWhereClauses(query, null);
@ -5081,19 +5112,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty))
{
whereClauses.Add("ItemId=@ItemId");
if (statement != null)
{
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
}
statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
}
if (!query.AppearsInItemId.Equals(Guid.Empty))
{
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
if (statement != null)
{
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@ -5101,10 +5126,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1)
{
whereClauses.Add("PersonType=@PersonType");
if (statement != null)
{
statement.TryBind("@PersonType", queryPersonTypes[0]);
}
statement?.TryBind("@PersonType", queryPersonTypes[0]);
}
else if (queryPersonTypes.Count > 1)
{
@ -5118,10 +5140,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1)
{
whereClauses.Add("PersonType<>@PersonType");
if (statement != null)
{
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
else if (queryExcludePersonTypes.Count > 1)
{
@ -5133,19 +5152,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue)
{
whereClauses.Add("ListOrder<=@MaxListOrder");
if (statement != null)
{
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
if (!string.IsNullOrWhiteSpace(query.NameContains))
{
whereClauses.Add("Name like @NameContains");
if (statement != null)
{
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
}
whereClauses.Add("p.Name like @NameContains");
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
}
if (query.IsFavorite.HasValue)
{
whereClauses.Add("isFavorite=@IsFavorite");
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
}
if (query.User != null)
{
statement?.TryBind("@UserId", query.User.InternalId);
}
return whereClauses;
@ -5359,7 +5383,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
itemCountColumns = new Dictionary<string, string>()
{
{ "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
{ "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
};
}
@ -5414,6 +5438,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags,
OfficialRatings = query.OfficialRatings,
StudioIds = query.StudioIds,
GenreIds = query.GenreIds,
Genres = query.Genres,
Years = query.Years,
@ -5586,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return counts;
}
var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToLookup(x => x);
foreach (var type in allTypes)
@ -5746,7 +5771,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = itemId.ToByteArray();
@ -5900,7 +5926,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = id.ToByteArray();
@ -6006,7 +6033,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
/// <summary>
/// Gets the chapter.
/// </summary>
@ -6235,7 +6261,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
connection.RunInTransaction(
db =>
{
var itemIdBlob = id.ToByteArray();

View File

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

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