Merge branch 'master' into network-rewrite

This commit is contained in:
Shadowghost 2023-06-15 17:53:52 +02:00
commit 32499f0e98
74 changed files with 2147 additions and 332 deletions

View File

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with: with:

View File

@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -51,14 +51,14 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Notify as running - name: Notify as running
id: comment_running id: comment_running
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -93,7 +93,7 @@ jobs:
exit ${retcode} exit ${retcode}
- name: Notify with result success - name: Notify with result success
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && success() }} if: ${{ github.event.comment != null && success() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -108,7 +108,7 @@ jobs:
reactions: hooray reactions: hooray
- name: Notify with result failure - name: Notify with result failure
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && failure() }} if: ${{ github.event.comment != null && failure() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -14,7 +14,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -39,7 +39,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -110,7 +110,7 @@ jobs:
direction: last direction: last
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body != '' }} if: ${{ steps.read-diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -125,7 +125,7 @@ jobs:
</details> </details>
- name: Edit difference comment (unchanged) - name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

View File

@ -165,6 +165,7 @@
- [MinecraftPlaye](https://github.com/MinecraftPlaye) - [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon) - [RealGreenDragon](https://github.com/RealGreenDragon)
- [ipitio](https://github.com/ipitio) - [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius)
# Emby Contributors # Emby Contributors

View File

@ -63,7 +63,7 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.0.2" /> <PackageVersion Include="SharpFuzz" Version="2.0.2" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />

View File

@ -49,20 +49,24 @@ namespace Emby.Dlna.PlayTo
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using MemoryStream ms = new MemoryStream();
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
try try
{ {
return await XDocument.LoadAsync( return await XDocument.LoadAsync(
stream, ms,
LoadOptions.None, LoadOptions.None,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
catch (XmlException) catch (XmlException)
{ {
// try correcting the Xml response with common errors // try correcting the Xml response with common errors
var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); ms.Position = 0;
using StreamReader sr = new StreamReader(ms);
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
// find and replace unescaped ampersands (&) // find and replace unescaped ampersands (&)
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;"); xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
@ -70,7 +74,7 @@ namespace Emby.Dlna.PlayTo
try try
{ {
// retry reading Xml // retry reading Xml
var xmlReader = new StringReader(xmlString); using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync( return await XDocument.LoadAsync(
xmlReader, xmlReader,
LoadOptions.None, LoadOptions.None,

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using MediaBrowser.Common.Providers; using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
char oldDirectorySeparatorChar; subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
char newDirectorySeparatorChar; path = path.NormalizePath(newDirectorySeparatorChar);
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
// The reasoning behind this is that a forward slash likely means it's a Linux path and
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
if (newSubPath.Contains('/', StringComparison.Ordinal))
{
oldDirectorySeparatorChar = '\\';
newDirectorySeparatorChar = '/';
}
else
{
oldDirectorySeparatorChar = '/';
newDirectorySeparatorChar = '\\';
}
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
// when the sub path matches a similar but in-complete subpath // when the sub path matches a similar but in-complete subpath
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
return true; return true;
} }
/// <summary>
/// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
/// </summary>
/// <param name="path">The path to canonicalize.</param>
/// <returns>The fully expanded, normalized path.</returns>
public static string Canonicalize(this string path)
{
return Path.GetFullPath(path).NormalizePath();
}
/// <summary>
/// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
/// </summary>
/// <param name="path">The path to normalize.</param>
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
[return: NotNullIfNotNull(nameof(path))]
public static string? NormalizePath(this string? path)
{
return path.NormalizePath(Path.DirectorySeparatorChar);
}
/// <summary>
/// Normalizes the path's directory separator character.
/// </summary>
/// <param name="path">The path to normalize.</param>
/// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
[return: NotNullIfNotNull(nameof(path))]
public static string? NormalizePath(this string? path, out char separator)
{
if (string.IsNullOrEmpty(path))
{
separator = default;
return path;
}
var newSeparator = '\\';
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
// The reasoning behind this is that a forward slash likely means it's a Linux path and
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
if (path.Contains('/', StringComparison.Ordinal))
{
newSeparator = '/';
}
separator = newSeparator;
return path.NormalizePath(newSeparator);
}
/// <summary>
/// Normalizes the path's directory separator character to the specified character.
/// </summary>
/// <param name="path">The path to normalize.</param>
/// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
/// <returns>The normalized path.</returns>
/// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
[return: NotNullIfNotNull(nameof(path))]
public static string? NormalizePath(this string? path, char newSeparator)
{
const char Bs = '\\';
const char Fs = '/';
if (!(newSeparator == Bs || newSeparator == Fs))
{
throw new ArgumentException("The character must be a directory separator.");
}
if (string.IsNullOrEmpty(path))
{
return path;
}
return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
}
} }
} }

View File

@ -81,7 +81,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if (season.IndexNumber.HasValue) if (season.IndexNumber.HasValue)
{ {
var seasonNumber = season.IndexNumber.Value; var seasonNumber = season.IndexNumber.Value;
if (string.IsNullOrEmpty(season.Name))
{
var seasonNames = series.SeasonNames;
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
{
season.Name = seasonName;
}
else
{
season.Name = seasonNumber == 0 ? season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName : args.LibraryOptions.SeasonZeroDisplayName :
string.Format( string.Format(
@ -90,6 +98,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
seasonNumber, seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage); args.LibraryOptions.PreferredMetadataLanguage);
} }
}
}
return season; return season;
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -9,6 +10,8 @@ using System.Runtime.Loader;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters; using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common; using MediaBrowser.Common;
@ -29,6 +32,8 @@ namespace Emby.Server.Implementations.Plugins
/// </summary> /// </summary>
public class PluginManager : IPluginManager public class PluginManager : IPluginManager
{ {
private const string MetafileName = "meta.json";
private readonly string _pluginsPath; private readonly string _pluginsPath;
private readonly Version _appVersion; private readonly Version _appVersion;
private readonly List<AssemblyLoadContext> _assemblyLoadContexts; private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
@ -44,7 +49,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class. /// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary> /// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param> /// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param>
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param> /// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
/// <param name="config">The <see cref="ServerConfiguration"/>.</param> /// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param> /// <param name="pluginsPath">The plugin path.</param>
@ -371,7 +376,7 @@ namespace Emby.Server.Implementations.Plugins
try try
{ {
var data = JsonSerializer.Serialize(manifest, _jsonOptions); var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data); File.WriteAllText(Path.Combine(path, MetafileName), data);
return true; return true;
} }
catch (ArgumentException e) catch (ArgumentException e)
@ -382,7 +387,7 @@ namespace Emby.Server.Implementations.Plugins
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status) public async Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
{ {
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString()); var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
var imagePath = string.Empty; var imagePath = string.Empty;
@ -427,9 +432,71 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath ImagePath = imagePath
}; };
if (!await ReconcileManifest(manifest, path))
{
// An error occurred during reconciliation and saving could be undesirable.
return false;
}
return SaveManifest(manifest, path); return SaveManifest(manifest, path);
} }
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
/// </summary>
/// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param>
/// <param name="path">The plugin path.</param>
/// <returns>The reconciled <see cref="PluginManifest"/>.</returns>
private async Task<bool> ReconcileManifest(PluginManifest manifest, string path)
{
try
{
var metafile = Path.Combine(path, MetafileName);
if (!File.Exists(metafile))
{
_logger.LogInformation("No local manifest exists for plugin {Plugin}. Skipping manifest reconciliation.", manifest.Name);
return true;
}
using var metaStream = File.OpenRead(metafile);
var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
localManifest ??= new PluginManifest();
if (!Equals(localManifest.Id, manifest.Id))
{
_logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id);
manifest.Status = PluginStatus.Malfunctioned;
}
if (localManifest.Version != manifest.Version)
{
// Package information provides the version and is the source of truth. Pre-packages meta.json is assumed to be a mistake in this regard.
_logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version);
}
// Explicitly mapping properties instead of using reflection is preferred here.
manifest.Category = string.IsNullOrEmpty(localManifest.Category) ? manifest.Category : localManifest.Category;
manifest.AutoUpdate = localManifest.AutoUpdate; // Preserve whatever is local. Package info does not have this property.
manifest.Changelog = string.IsNullOrEmpty(localManifest.Changelog) ? manifest.Changelog : localManifest.Changelog;
manifest.Description = string.IsNullOrEmpty(localManifest.Description) ? manifest.Description : localManifest.Description;
manifest.Name = string.IsNullOrEmpty(localManifest.Name) ? manifest.Name : localManifest.Name;
manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Overview;
manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner;
manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest.TargetAbi;
manifest.Timestamp = localManifest.Timestamp.Equals(default) ? manifest.Timestamp : localManifest.Timestamp;
manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest.ImagePath;
manifest.Assemblies = localManifest.Assemblies;
return true;
}
catch (Exception e)
{
_logger.LogWarning(e, "Unable to reconcile plugin manifest due to an error. {Path}", path);
return false;
}
}
/// <summary> /// <summary>
/// Changes a plugin's load status. /// Changes a plugin's load status.
/// </summary> /// </summary>
@ -594,7 +661,7 @@ namespace Emby.Server.Implementations.Plugins
{ {
Version? version; Version? version;
PluginManifest? manifest = null; PluginManifest? manifest = null;
var metafile = Path.Combine(dir, "meta.json"); var metafile = Path.Combine(dir, MetafileName);
if (File.Exists(metafile)) if (File.Exists(metafile))
{ {
// Only path where this stays null is when File.ReadAllBytes throws an IOException // Only path where this stays null is when File.ReadAllBytes throws an IOException
@ -688,7 +755,15 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x]; var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{ {
entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories); if (!TryGetPluginDlls(entry, out var allowedDlls))
{
_logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name);
ChangePluginState(entry, PluginStatus.Malfunctioned);
continue;
}
entry.DllFiles = allowedDlls;
if (entry.IsEnabledAndSupported) if (entry.IsEnabledAndSupported)
{ {
lastName = entry.Name; lastName = entry.Name;
@ -734,6 +809,68 @@ namespace Emby.Server.Implementations.Plugins
return versions.Where(p => p.DllFiles.Count != 0); return versions.Where(p => p.DllFiles.Count != 0);
} }
/// <summary>
/// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist
/// from the manifest.
/// </summary>
/// <remarks>
/// Loading DLLs from externally supplied paths introduces a path traversal risk. This method
/// uses a safelisting tactic of considering DLLs from the plugin directory and only using
/// the plugin's canonicalized assembly whitelist for comparison. See
/// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details.
/// </remarks>
/// <param name="plugin">The plugin.</param>
/// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will be empty.</param>
/// <returns>
/// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory.
/// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory.
/// </returns>
/// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception>
private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls)
{
ArgumentNullException.ThrowIfNull(nameof(plugin));
IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories);
whitelistedDlls = Array.Empty<string>();
if (pluginDlls.Count > 0 && plugin.Manifest.Assemblies.Count > 0)
{
_logger.LogInformation("Registering whitelisted assemblies for plugin \"{Plugin}\"...", plugin.Name);
var canonicalizedPaths = new List<string>();
foreach (var path in plugin.Manifest.Assemblies)
{
var canonicalized = Path.Combine(plugin.Path, path).Canonicalize();
// Ensure we stay in the plugin directory.
if (!canonicalized.StartsWith(plugin.Path.NormalizePath(), StringComparison.Ordinal))
{
_logger.LogError("Assembly path {Path} is not inside the plugin directory.", path);
return false;
}
canonicalizedPaths.Add(canonicalized);
}
var intersected = pluginDlls.Intersect(canonicalizedPaths).ToList();
if (intersected.Count != canonicalizedPaths.Count)
{
_logger.LogError("Plugin {Plugin} contained assembly paths that were not found in the directory.", plugin.Name);
return false;
}
whitelistedDlls = intersected;
}
else
{
// No whitelist, default to loading all DLLs in plugin directory.
whitelistedDlls = pluginDlls;
}
return true;
}
/// <summary> /// <summary>
/// Changes the status of the other versions of the plugin to "Superceded". /// Changes the status of the other versions of the plugin to "Superceded".
/// </summary> /// </summary>

View File

@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.Updates
var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber); var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin is not null) if (plugin is not null)
{ {
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
} }
// Remove versions with a target ABI greater then the current application version. // Remove versions with a target ABI greater then the current application version.
@ -555,7 +555,10 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0; stream.Position = 0;
using var reader = new ZipArchive(stream); using var reader = new ZipArchive(stream);
reader.ExtractToDirectory(targetDir, true); reader.ExtractToDirectory(targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
// Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
_pluginManager.ImportPluginFrom(targetDir); _pluginManager.ImportPluginFrom(targetDir);
} }

View File

@ -1,12 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Server.Migrations; using Jellyfin.Server.Migrations;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.ApiClient; using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay; using MediaBrowser.Model.SyncPlay;
using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Any;
@ -36,17 +40,141 @@ namespace Jellyfin.Server.Filters
/// <inheritdoc /> /// <inheritdoc />
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{ {
context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository); var webSocketTypes = typeof(WebSocketMessage).Assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(WebSocketMessage))
&& !t.IsGenericType
&& t != typeof(WebSocketMessageInfo))
.ToList();
var inboundWebSocketSchemas = new List<OpenApiSchema>();
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
if (messageType is null)
{
continue;
}
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
inboundWebSocketSchemas.Add(schema);
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
}
var inboundWebSocketMessageSchema = new OpenApiSchema
{
Type = "object",
Description = "Represents the list of possible inbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(InboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = inboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(WebSocketMessage.MessageType),
Mapping = inboundWebSocketDiscriminators
}
};
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
var outboundWebSocketSchemas = new List<OpenApiSchema>();
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
if (messageType is null)
{
continue;
}
// Additional discriminator needed for GroupUpdate models...
if (messageType == SessionMessageType.SyncPlayGroupUpdate && type != typeof(SyncPlayGroupUpdateCommandMessage))
{
continue;
}
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
outboundWebSocketSchemas.Add(schema);
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
}
var outboundWebSocketMessageSchema = new OpenApiSchema
{
Type = "object",
Description = "Represents the list of possible outbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(OutboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = outboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(WebSocketMessage.MessageType),
Mapping = outboundWebSocketDiscriminators
}
};
context.SchemaRepository.AddDefinition(nameof(OutboundWebSocketMessage), outboundWebSocketMessageSchema);
context.SchemaRepository.AddDefinition(
nameof(WebSocketMessage),
new OpenApiSchema
{
Type = "object",
Description = "Represents the possible websocket types",
Reference = new OpenApiReference
{
Id = nameof(WebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = new[]
{
inboundWebSocketMessageSchema,
outboundWebSocketMessageSchema
}
});
// Manually generate sync play GroupUpdate messages.
if (!context.SchemaRepository.Schemas.TryGetValue(nameof(GroupUpdate), out var groupUpdateSchema))
{
groupUpdateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository);
}
var groupUpdateOfGroupInfoSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupInfoDto>), context.SchemaRepository);
var groupUpdateOfGroupStateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupStateUpdate>), context.SchemaRepository);
var groupUpdateOfStringSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<string>), context.SchemaRepository);
var groupUpdateOfPlayQueueSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<PlayQueueUpdate>), context.SchemaRepository);
groupUpdateSchema.OneOf = new List<OpenApiSchema>
{
groupUpdateOfGroupInfoSchema,
groupUpdateOfGroupStateSchema,
groupUpdateOfStringSchema,
groupUpdateOfPlayQueueSchema
};
groupUpdateSchema.Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(GroupUpdate.Type),
Mapping = new Dictionary<string, string>
{
{ GroupUpdateType.UserJoined.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.UserLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupJoined.ToString(), groupUpdateOfGroupInfoSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.StateUpdate.ToString(), groupUpdateOfGroupStateSchema.Reference.ReferenceV3 },
{ GroupUpdateType.PlayQueue.ToString(), groupUpdateOfPlayQueueSchema.Reference.ReferenceV3 },
{ GroupUpdateType.NotInGroup.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupDoesNotExist.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.LibraryAccessDenied.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }
}
};
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores()) foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())

View File

@ -57,7 +57,7 @@ namespace MediaBrowser.Common.Plugins
/// <param name="path">The path where to save the manifest.</param> /// <param name="path">The path where to save the manifest.</param>
/// <param name="status">Initial status of the plugin.</param> /// <param name="status">Initial status of the plugin.</param>
/// <returns>True if successful.</returns> /// <returns>True if successful.</returns>
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status); Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
/// <summary> /// <summary>
/// Imports plugin details from a folder. /// Imports plugin details from a folder.

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
@ -23,6 +24,7 @@ namespace MediaBrowser.Common.Plugins
Overview = string.Empty; Overview = string.Empty;
TargetAbi = string.Empty; TargetAbi = string.Empty;
Version = string.Empty; Version = string.Empty;
Assemblies = Array.Empty<string>();
} }
/// <summary> /// <summary>
@ -104,5 +106,12 @@ namespace MediaBrowser.Common.Plugins
/// </summary> /// </summary>
[JsonPropertyName("imagePath")] [JsonPropertyName("imagePath")]
public string? ImagePath { get; set; } public string? ImagePath { get; set; }
/// <summary>
/// Gets or sets the collection of assemblies that should be loaded.
/// Paths are considered relative to the plugin folder.
/// </summary>
[JsonPropertyName("assemblies")]
public IReadOnlyList<string> Assemblies { get; set; }
} }
} }

View File

@ -28,12 +28,16 @@ namespace MediaBrowser.Controller.Entities.TV
public Series() public Series()
{ {
AirDays = Array.Empty<DayOfWeek>(); AirDays = Array.Empty<DayOfWeek>();
SeasonNames = new Dictionary<int, string>();
} }
public DayOfWeek[] AirDays { get; set; } public DayOfWeek[] AirDays { get; set; }
public string AirTime { get; set; } public string AirTime { get; set; }
[JsonIgnore]
public Dictionary<int, string> SeasonNames { get; set; }
[JsonIgnore] [JsonIgnore]
public override bool SupportsAddingToPlaylist => true; public override bool SupportsAddingToPlaylist => true;

View File

@ -45,6 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private static readonly string[] _videoProfilesH264 = new[] private static readonly string[] _videoProfilesH264 = new[]
{ {
@ -162,7 +163,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsVaapiFullSupported() private bool IsVaapiFullSupported()
{ {
return _mediaEncoder.SupportsHwaccel("vaapi") return _mediaEncoder.SupportsHwaccel("drm")
&& _mediaEncoder.SupportsHwaccel("vaapi")
&& _mediaEncoder.SupportsFilter("scale_vaapi") && _mediaEncoder.SupportsFilter("scale_vaapi")
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi") && _mediaEncoder.SupportsFilter("deinterlace_vaapi")
&& _mediaEncoder.SupportsFilter("tonemap_vaapi") && _mediaEncoder.SupportsFilter("tonemap_vaapi")
@ -712,28 +714,43 @@ namespace MediaBrowser.Controller.MediaEncoding
options); options);
} }
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias) private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
{ {
alias ??= VaapiAlias; alias ??= VaapiAlias;
renderNodePath = renderNodePath ?? "/dev/dri/renderD128"; renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
var options = string.IsNullOrEmpty(driver) var driverOpts = string.IsNullOrEmpty(driver)
? renderNodePath ? ":" + renderNodePath
: ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver); : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
var options = string.IsNullOrEmpty(srcDeviceAlias)
? driverOpts
: "@" + srcDeviceAlias;
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
" -init_hw_device vaapi={0}:{1}", " -init_hw_device vaapi={0}{1}",
alias, alias,
options); options);
} }
private string GetDrmDeviceArgs(string renderNodePath, string alias)
{
alias ??= DrmAlias;
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
return string.Format(
CultureInfo.InvariantCulture,
" -init_hw_device drm={0}:{1}",
alias,
renderNodePath);
}
private string GetQsvDeviceArgs(string alias) private string GetQsvDeviceArgs(string alias)
{ {
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias); var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
if (OperatingSystem.IsLinux()) if (OperatingSystem.IsLinux())
{ {
// derive qsv from vaapi device // derive qsv from vaapi device
return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias; return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
} }
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
@ -754,9 +771,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetGraphicalSubCanvasSize(EncodingJobInfo state) public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
{ {
// DVBSUB and DVDSUB use the fixed canvas size 720x576
if (state.SubtitleStream is not null if (state.SubtitleStream is not null
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
&& !state.SubtitleStream.IsTextSubtitleStream) && !state.SubtitleStream.IsTextSubtitleStream
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
{ {
var inW = state.VideoStream?.Width; var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height; var inH = state.VideoStream?.Height;
@ -824,21 +844,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD) if (_mediaEncoder.IsVaapiDeviceInteliHD)
{ {
args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias)); args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
} }
else if (_mediaEncoder.IsVaapiDeviceInteli965) else if (_mediaEncoder.IsVaapiDeviceInteli965)
{ {
// Only override i965 since it has lower priority than iHD in libva lookup. // Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias)); args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
}
else
{
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
} }
var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); var filterDevArgs = string.Empty;
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported(); var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965) if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
@ -855,17 +871,26 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{ {
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias));
// libplacebo wants an explicitly set vulkan filter device. // libplacebo wants an explicitly set vulkan filter device.
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias); filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
} }
else if (doOclTonemap) else
{
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias));
filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
if (doOclTonemap)
{ {
// ROCm/ROCr OpenCL runtime // ROCm/ROCr OpenCL runtime
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
} }
} }
}
else if (doOclTonemap) else if (doOclTonemap)
{ {
args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias)); args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
@ -1549,11 +1574,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -preset p7"; param += " -preset p7";
break; break;
case "slow": case "slower":
param += " -preset p6"; param += " -preset p6";
break; break;
case "slower": case "slow":
param += " -preset p5"; param += " -preset p5";
break; break;
@ -1586,8 +1611,8 @@ namespace MediaBrowser.Controller.MediaEncoding
switch (encodingOptions.EncoderPreset) switch (encodingOptions.EncoderPreset)
{ {
case "veryslow": case "veryslow":
case "slow":
case "slower": case "slower":
case "slow":
param += " -quality quality"; param += " -quality quality";
break; break;
@ -2929,7 +2954,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty; return string.Empty;
} }
public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
{ {
if (string.IsNullOrEmpty(hwTonemapSuffix)) if (string.IsNullOrEmpty(hwTonemapSuffix))
{ {
@ -2941,7 +2966,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{ {
args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16"; args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32";
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
args, args,
@ -2972,14 +2998,24 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
if (options.TonemappingParam != 0) if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase)
|| string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase))
{ {
args += ":param={5}"; if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode)
{
args += ":tonemap_mode={5}";
}
} }
if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) if (options.TonemappingParam != 0)
{ {
args += ":range={6}"; args += ":param={6}";
}
if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
{
args += ":range={7}";
} }
} }
@ -2991,10 +3027,80 @@ namespace MediaBrowser.Controller.MediaEncoding
algorithm, algorithm,
options.TonemappingPeak, options.TonemappingPeak,
options.TonemappingDesat, options.TonemappingDesat,
options.TonemappingMode,
options.TonemappingParam, options.TonemappingParam,
options.TonemappingRange); options.TonemappingRange);
} }
public string GetLibplaceboFilter(
EncodingOptions options,
string videoFormat,
bool doTonemap,
int? videoWidth,
int? videoHeight,
int? requestedWidth,
int? requestedHeight,
int? requestedMaxWidth,
int? requestedMaxHeight)
{
var (outWidth, outHeight) = GetFixedOutputSize(
videoWidth,
videoHeight,
requestedWidth,
requestedHeight,
requestedMaxWidth,
requestedMaxHeight);
var isFormatFixed = !string.IsNullOrEmpty(videoFormat);
var isSizeFixed = !videoWidth.HasValue
|| outWidth.Value != videoWidth.Value
|| !videoHeight.HasValue
|| outHeight.Value != videoHeight.Value;
var sizeArg = isSizeFixed ? (":w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty;
var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty;
var tonemapArg = string.Empty;
if (doTonemap)
{
var algorithm = options.TonemappingAlgorithm;
var mode = options.TonemappingMode;
var range = options.TonemappingRange;
if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
{
algorithm = "bt.2390";
}
else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase))
{
algorithm = "clip";
}
tonemapArg = ":tonemapping=" + algorithm;
if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase))
{
tonemapArg += ":tonemapping_mode=" + mode;
}
tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
{
tonemapArg += ":range=" + range;
}
}
return string.Format(
CultureInfo.InvariantCulture,
"libplacebo=upscaler=none:downscaler=none{0}{1}{2}",
sizeArg,
formatArg,
tonemapArg);
}
/// <summary> /// <summary>
/// Gets the parameter of software filter chain. /// Gets the parameter of software filter chain.
/// </summary> /// </summary>
@ -4224,7 +4330,6 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isVaapiEncoder; var isSwEncoder = !isVaapiEncoder;
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
@ -4253,26 +4358,31 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter); mainFilters.Add(swDeintFilter);
} }
var outFormat = doVkTonemap ? "yuv420p10le" : "nv12"; if (doVkTonemap || hasSubs)
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
mainFilters.Add("format=" + outFormat);
// keep video at memory except vk tonemap,
// since the overhead caused by hwupload >>> using sw filter.
// sw => hw
if (doVkTonemap)
{ {
mainFilters.Add("hwupload=derive_device=vaapi"); // sw => hw
mainFilters.Add("format=vaapi"); mainFilters.Add("hwupload=derive_device=vulkan");
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan"); mainFilters.Add("format=vulkan");
} }
else
{
// sw scale
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(swScaleFilter);
mainFilters.Add("format=nv12");
}
} }
else if (isVaapiDecoder) else if (isVaapiDecoder)
{ {
// INPUT vaapi surface(vram) // INPUT vaapi surface(vram)
if (doVkTonemap || hasSubs)
{
// map from vaapi to vulkan/drm via interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan");
}
else
{
// hw deint // hw deint
if (doDeintH2645) if (doDeintH2645)
{ {
@ -4280,72 +4390,49 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(deintFilter); mainFilters.Add(deintFilter);
} }
var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12");
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for overlay_vulkan
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs)
{
hwScaleFilter += ":extra_hw_frames=32";
}
// hw scale // hw scale
var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH);
mainFilters.Add(hwScaleFilter); mainFilters.Add(hwScaleFilter);
} }
if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
{
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan");
} }
// vk tonemap // vk libplacebo
if (doVkTonemap) if (doVkTonemap || hasSubs)
{ {
var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12"; var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat); mainFilters.Add(libplaceboFilter);
mainFilters.Add(tonemapFilter);
} }
if (doVkTonemap && isVaInVaOut && !hasSubs) if (doVkTonemap && !hasSubs)
{ {
// OUTPUT vaapi(nv12/bgra) surface(vram) // OUTPUT vaapi(nv12) surface(vram)
// reverse-mapping via vaapi-vulkan interop. // map from vulkan/drm to vaapi via interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vaapi:reverse=1"); mainFilters.Add("hwmap=derive_device=drm");
mainFilters.Add("format=drm_prime");
mainFilters.Add("hwmap=derive_device=vaapi");
mainFilters.Add("format=vaapi"); mainFilters.Add("format=vaapi");
// clear the surf->meta_offset and output nv12
mainFilters.Add("scale_vaapi=format=nv12");
// hw deint
if (doDeintH2645)
{
var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
mainFilters.Add(deintFilter);
}
} }
var memoryOutput = false; if (!hasSubs)
var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
{ {
memoryOutput = true;
// OUTPUT nv12 surface(memory) // OUTPUT nv12 surface(memory)
if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
{
mainFilters.Add("hwdownload"); mainFilters.Add("hwdownload");
mainFilters.Add("format=nv12"); mainFilters.Add("format=nv12");
} }
// OUTPUT nv12 surface(memory) if (isSwDecoder && isVaapiEncoder && !doVkTonemap)
if (isSwDecoder && isVaapiEncoder)
{
memoryOutput = true;
}
if (memoryOutput)
{
// text subtitles
if (hasTextSubs)
{
var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
mainFilters.Add(textSubtitlesFilter);
}
}
if (memoryOutput && isVaapiEncoder)
{
if (!hasGraphicalSubs)
{ {
mainFilters.Add("hwupload_vaapi"); mainFilters.Add("hwupload_vaapi");
} }
@ -4354,8 +4441,6 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make sub and overlay filters for subtitle stream */ /* Make sub and overlay filters for subtitle stream */
var subFilters = new List<string>(); var subFilters = new List<string>();
var overlayFilters = new List<string>(); var overlayFilters = new List<string>();
if (isVaInVaOut)
{
if (hasSubs) if (hasSubs)
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
@ -4374,35 +4459,35 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter); subFilters.Add(subTextSubtitlesFilter);
} }
// prefer vaapi hwupload to vulkan hwupload, subFilters.Add("hwupload=derive_device=vulkan");
// Mesa RADV does not support a dedicated transfer queue.
subFilters.Add("hwupload=derive_device=vaapi");
subFilters.Add("format=vaapi");
subFilters.Add("hwmap=derive_device=vulkan");
subFilters.Add("format=vulkan"); subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
// TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi. if (isSwEncoder)
overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none"); {
// OUTPUT nv12 surface(memory)
// OUTPUT vaapi(nv12/bgra) surface(vram) overlayFilters.Add("scale_vulkan=format=nv12");
// reverse-mapping via vaapi-vulkan interop. overlayFilters.Add("hwdownload");
overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1"); overlayFilters.Add("format=nv12");
}
else if (isVaapiEncoder)
{
// OUTPUT vaapi(nv12) surface(vram)
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
overlayFilters.Add("hwmap=derive_device=drm");
overlayFilters.Add("format=drm_prime");
overlayFilters.Add("hwmap=derive_device=vaapi");
overlayFilters.Add("format=vaapi"); overlayFilters.Add("format=vaapi");
}
}
else if (memoryOutput)
{
if (hasGraphicalSubs)
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
if (isVaapiEncoder) // clear the surf->meta_offset and output nv12
overlayFilters.Add("scale_vaapi=format=nv12");
// hw deint
if (doDeintH2645)
{ {
overlayFilters.Add("hwupload_vaapi"); var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
overlayFilters.Add(deintFilter);
} }
} }
} }

View File

@ -0,0 +1,28 @@
using System;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net;
/// <summary>
/// Websocket message without data.
/// </summary>
public abstract class WebSocketMessage
{
/// <summary>
/// Gets or sets the type of the message.
/// TODO make this abstract and get only.
/// </summary>
public virtual SessionMessageType MessageType { get; set; }
/// <summary>
/// Gets or sets the message id.
/// </summary>
public Guid MessageId { get; set; }
/// <summary>
/// Gets or sets the server id.
/// </summary>
[JsonIgnore]
public string? ServerId { get; set; }
}

View File

@ -0,0 +1,33 @@
#pragma warning disable SA1649 // File name must equal class name.
namespace MediaBrowser.Controller.Net;
/// <summary>
/// Class WebSocketMessage.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
// TODO make this abstract, remove empty ctor.
public class WebSocketMessage<T> : WebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class.
/// </summary>
public WebSocketMessage()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class.
/// </summary>
/// <param name="data">The data to send.</param>
protected WebSocketMessage(T data)
{
Data = data;
}
/// <summary>
/// Gets or sets the data.
/// </summary>
// TODO make this set only.
public T? Data { get; set; }
}

View File

@ -0,0 +1,10 @@
#pragma warning disable CA1040
namespace MediaBrowser.Controller.Net.WebSocketMessages;
/// <summary>
/// Interface representing that the websocket message is inbound.
/// </summary>
public interface IInboundWebSocketMessage
{
}

View File

@ -0,0 +1,10 @@
#pragma warning disable CA1040
namespace MediaBrowser.Controller.Net.WebSocketMessages;
/// <summary>
/// Interface representing that the websocket message is outbound.
/// </summary>
public interface IOutboundWebSocketMessage
{
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Activity log entry start message.
/// </summary>
public class ActivityLogEntryStartMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class.
/// </summary>
/// <param name="data">Collection of activity log entries.</param>
public ActivityLogEntryStartMessage(IReadOnlyCollection<ActivityLogEntry> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ActivityLogEntryStart)]
public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStart;
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Activity log entry stop message.
/// </summary>
public class ActivityLogEntryStopMessage : WebSocketMessage<IReadOnlyCollection<ActivityLogEntry>>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryStopMessage"/> class.
/// </summary>
/// <param name="data">Collection of activity log entries.</param>
public ActivityLogEntryStopMessage(IReadOnlyCollection<ActivityLogEntry> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ActivityLogEntryStop)]
public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop;
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Scheduled tasks info start message.
/// </summary>
public class ScheduledTasksInfoStartMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class.
/// </summary>
/// <param name="data">Collection of task info.</param>
public ScheduledTasksInfoStartMessage(IReadOnlyCollection<TaskInfo> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ScheduledTasksInfoStart)]
public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStart;
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Scheduled tasks info stop message.
/// </summary>
public class ScheduledTasksInfoStopMessage : WebSocketMessage<IReadOnlyCollection<TaskInfo>>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTasksInfoStopMessage"/> class.
/// </summary>
/// <param name="data">Collection of task info.</param>
public ScheduledTasksInfoStopMessage(IReadOnlyCollection<TaskInfo> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ScheduledTasksInfoStop)]
public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Sessions start message.
/// </summary>
public class SessionsStartMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SessionsStartMessage"/> class.
/// </summary>
/// <param name="data">Session info.</param>
public SessionsStartMessage(SessionInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SessionsStart)]
public override SessionMessageType MessageType => SessionMessageType.SessionsStart;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
/// <summary>
/// Sessions stop message.
/// </summary>
public class SessionsStopMessage : WebSocketMessage<SessionInfo>, IInboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SessionsStopMessage"/> class.
/// </summary>
/// <param name="data">Session info.</param>
public SessionsStopMessage(SessionInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SessionsStop)]
public override SessionMessageType MessageType => SessionMessageType.SessionsStop;
}

View File

@ -0,0 +1,9 @@
namespace MediaBrowser.Controller.Net.WebSocketMessages;
/// <summary>
/// Class representing the list of outbound websocket message types.
/// Only used in openapi generation.
/// </summary>
public class InboundWebSocketMessage : WebSocketMessage
{
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Activity log created message.
/// </summary>
public class ActivityLogEntryMessage : WebSocketMessage<IReadOnlyList<ActivityLogEntry>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class.
/// </summary>
/// <param name="data">List of activity log entries.</param>
public ActivityLogEntryMessage(IReadOnlyList<ActivityLogEntry> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ActivityLogEntry)]
public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntry;
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Force keep alive websocket messages.
/// </summary>
public class ForceKeepAliveMessage : WebSocketMessage<int>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class.
/// </summary>
/// <param name="data">The timeout in seconds.</param>
public ForceKeepAliveMessage(int data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ForceKeepAlive)]
public override SessionMessageType MessageType => SessionMessageType.ForceKeepAlive;
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// General command websocket message.
/// </summary>
public class GeneralCommandMessage : WebSocketMessage<GeneralCommand>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class.
/// </summary>
/// <param name="data">The general command.</param>
public GeneralCommandMessage(GeneralCommand data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.GeneralCommand)]
public override SessionMessageType MessageType => SessionMessageType.GeneralCommand;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Library changed message.
/// </summary>
public class LibraryChangedMessage : WebSocketMessage<LibraryUpdateInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class.
/// </summary>
/// <param name="data">The library update info.</param>
public LibraryChangedMessage(LibraryUpdateInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.LibraryChanged)]
public override SessionMessageType MessageType => SessionMessageType.LibraryChanged;
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Play command websocket message.
/// </summary>
public class PlayMessage : WebSocketMessage<PlayRequest>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PlayMessage"/> class.
/// </summary>
/// <param name="data">The play request.</param>
public PlayMessage(PlayRequest data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.Play)]
public override SessionMessageType MessageType => SessionMessageType.Play;
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Playstate message.
/// </summary>
public class PlaystateMessage : WebSocketMessage<PlaystateRequest>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PlaystateMessage"/> class.
/// </summary>
/// <param name="data">Playstate request data.</param>
public PlaystateMessage(PlaystateRequest data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.Playstate)]
public override SessionMessageType MessageType => SessionMessageType.Playstate;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Plugin installation cancelled message.
/// </summary>
public class PluginInstallationCancelledMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class.
/// </summary>
/// <param name="data">Installation info.</param>
public PluginInstallationCancelledMessage(InstallationInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.PackageInstallationCancelled)]
public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCancelled;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Plugin installation completed message.
/// </summary>
public class PluginInstallationCompletedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class.
/// </summary>
/// <param name="data">Installation info.</param>
public PluginInstallationCompletedMessage(InstallationInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.PackageInstallationCompleted)]
public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCompleted;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Plugin installation failed message.
/// </summary>
public class PluginInstallationFailedMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class.
/// </summary>
/// <param name="data">Installation info.</param>
public PluginInstallationFailedMessage(InstallationInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.PackageInstallationFailed)]
public override SessionMessageType MessageType => SessionMessageType.PackageInstallationFailed;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Package installing message.
/// </summary>
public class PluginInstallingMessage : WebSocketMessage<InstallationInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class.
/// </summary>
/// <param name="data">Installation info.</param>
public PluginInstallingMessage(InstallationInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.PackageInstalling)]
public override SessionMessageType MessageType => SessionMessageType.PackageInstalling;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Plugin uninstalled message.
/// </summary>
public class PluginUninstalledMessage : WebSocketMessage<PluginInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class.
/// </summary>
/// <param name="data">Plugin info.</param>
public PluginUninstalledMessage(PluginInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.PackageUninstalled)]
public override SessionMessageType MessageType => SessionMessageType.PackageUninstalled;
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Refresh progress message.
/// </summary>
public class RefreshProgressMessage : WebSocketMessage<Dictionary<string, string>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class.
/// </summary>
/// <param name="data">Refresh progress data.</param>
public RefreshProgressMessage(Dictionary<string, string> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.RefreshProgress)]
public override SessionMessageType MessageType => SessionMessageType.RefreshProgress;
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Restart required.
/// </summary>
public class RestartRequiredMessage : WebSocketMessage, IOutboundWebSocketMessage
{
/// <inheritdoc />
[DefaultValue(SessionMessageType.RestartRequired)]
public override SessionMessageType MessageType => SessionMessageType.RestartRequired;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Scheduled task ended message.
/// </summary>
public class ScheduledTaskEndedMessage : WebSocketMessage<TaskResult>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class.
/// </summary>
/// <param name="data">Task result.</param>
public ScheduledTaskEndedMessage(TaskResult data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ScheduledTaskEnded)]
public override SessionMessageType MessageType => SessionMessageType.ScheduledTaskEnded;
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Scheduled tasks info message.
/// </summary>
public class ScheduledTasksInfoMessage : WebSocketMessage<IReadOnlyList<TaskInfo>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class.
/// </summary>
/// <param name="data">List of task infos.</param>
public ScheduledTasksInfoMessage(IReadOnlyList<TaskInfo> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.ScheduledTasksInfo)]
public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfo;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Series timer cancelled message.
/// </summary>
public class SeriesTimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class.
/// </summary>
/// <param name="data">The timer event info.</param>
public SeriesTimerCancelledMessage(TimerEventInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SeriesTimerCancelled)]
public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCancelled;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Series timer created message.
/// </summary>
public class SeriesTimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class.
/// </summary>
/// <param name="data">timer event info.</param>
public SeriesTimerCreatedMessage(TimerEventInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SeriesTimerCreated)]
public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCreated;
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Server restarting down message.
/// </summary>
public class ServerRestartingMessage : WebSocketMessage, IOutboundWebSocketMessage
{
/// <inheritdoc />
[DefaultValue(SessionMessageType.ServerRestarting)]
public override SessionMessageType MessageType => SessionMessageType.ServerRestarting;
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Server shutting down message.
/// </summary>
public class ServerShuttingDownMessage : WebSocketMessage, IOutboundWebSocketMessage
{
/// <inheritdoc />
[DefaultValue(SessionMessageType.ServerShuttingDown)]
public override SessionMessageType MessageType => SessionMessageType.ServerShuttingDown;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sessions message.
/// </summary>
public class SessionsMessage : WebSocketMessage<SessionInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SessionsMessage"/> class.
/// </summary>
/// <param name="data">Session info.</param>
public SessionsMessage(SessionInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.Sessions)]
public override SessionMessageType MessageType => SessionMessageType.Sessions;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play command.
/// </summary>
public class SyncPlayCommandMessage : WebSocketMessage<SendCommand>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class.
/// </summary>
/// <param name="data">The send command.</param>
public SyncPlayCommandMessage(SendCommand data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayCommand)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayCommand;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Untyped sync play command.
/// </summary>
public class SyncPlayGroupUpdateCommandMessage : WebSocketMessage<GroupUpdate>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class.
/// </summary>
/// <param name="data">The send command.</param>
public SyncPlayGroupUpdateCommandMessage(GroupUpdate data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with group info.
/// GroupUpdateTypes: GroupJoined.
/// </summary>
public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : WebSocketMessage<GroupUpdate<GroupInfoDto>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class.
/// </summary>
/// <param name="data">The group info.</param>
public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with group state update.
/// GroupUpdateTypes: StateUpdate.
/// </summary>
public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : WebSocketMessage<GroupUpdate<GroupStateUpdate>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class.
/// </summary>
/// <param name="data">The group info.</param>
public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with play queue update.
/// GroupUpdateTypes: PlayQueue.
/// </summary>
public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : WebSocketMessage<GroupUpdate<PlayQueueUpdate>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class.
/// </summary>
/// <param name="data">The play queue update.</param>
public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with string.
/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username).
/// </summary>
public class SyncPlayGroupUpdateCommandOfStringMessage : WebSocketMessage<GroupUpdate<string>>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class.
/// </summary>
/// <param name="data">The send command.</param>
public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Timer cancelled message.
/// </summary>
public class TimerCancelledMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class.
/// </summary>
/// <param name="data">Timer event info.</param>
public TimerCancelledMessage(TimerEventInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.TimerCancelled)]
public override SessionMessageType MessageType => SessionMessageType.TimerCancelled;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Timer created message.
/// </summary>
public class TimerCreatedMessage : WebSocketMessage<TimerEventInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class.
/// </summary>
/// <param name="data">Timer event info.</param>
public TimerCreatedMessage(TimerEventInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.TimerCreated)]
public override SessionMessageType MessageType => SessionMessageType.TimerCreated;
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// User data changed message.
/// </summary>
public class UserDataChangedMessage : WebSocketMessage<UserDataChangeInfo>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class.
/// </summary>
/// <param name="data">The data change info.</param>
public UserDataChangedMessage(UserDataChangeInfo data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.UserDataChanged)]
public override SessionMessageType MessageType => SessionMessageType.UserDataChanged;
}

View File

@ -0,0 +1,24 @@
using System;
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// User deleted message.
/// </summary>
public class UserDeletedMessage : WebSocketMessage<Guid>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="UserDeletedMessage"/> class.
/// </summary>
/// <param name="data">The user id.</param>
public UserDeletedMessage(Guid data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.UserDeleted)]
public override SessionMessageType MessageType => SessionMessageType.UserDeleted;
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// User updated message.
/// </summary>
public class UserUpdatedMessage : WebSocketMessage<UserDto>, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class.
/// </summary>
/// <param name="data">The user dto.</param>
public UserUpdatedMessage(UserDto data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.UserUpdated)]
public override SessionMessageType MessageType => SessionMessageType.UserUpdated;
}

View File

@ -0,0 +1,9 @@
namespace MediaBrowser.Controller.Net.WebSocketMessages;
/// <summary>
/// Class representing the list of outbound websocket message types.
/// Only used in openapi generation.
/// </summary>
public class OutboundWebSocketMessage : WebSocketMessage
{
}

View File

@ -0,0 +1,23 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Shared;
/// <summary>
/// Keep alive websocket messages.
/// </summary>
public class KeepAliveMessage : WebSocketMessage<int>, IInboundWebSocketMessage, IOutboundWebSocketMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="KeepAliveMessage"/> class.
/// </summary>
/// <param name="data">The seconds to keep alive for.</param>
public KeepAliveMessage(int data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.KeepAlive)]
public override SessionMessageType MessageType => SessionMessageType.KeepAlive;
}

View File

@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// The sorted playlist. /// The sorted playlist.
/// </summary> /// </summary>
/// <value>The sorted playlist, or play queue of the group.</value> /// <value>The sorted playlist, or play queue of the group.</value>
private List<QueueItem> _sortedPlaylist = new List<QueueItem>(); private List<SyncPlayQueueItem> _sortedPlaylist = new List<SyncPlayQueueItem>();
/// <summary> /// <summary>
/// The shuffled playlist. /// The shuffled playlist.
/// </summary> /// </summary>
/// <value>The shuffled playlist, or play queue of the group.</value> /// <value>The shuffled playlist, or play queue of the group.</value>
private List<QueueItem> _shuffledPlaylist = new List<QueueItem>(); private List<SyncPlayQueueItem> _shuffledPlaylist = new List<SyncPlayQueueItem>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class. /// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playlist considering the shuffle mode. /// Gets the current playlist considering the shuffle mode.
/// </summary> /// </summary>
/// <returns>The playlist.</returns> /// <returns>The playlist.</returns>
public IReadOnlyList<QueueItem> GetPlaylist() public IReadOnlyList<SyncPlayQueueItem> GetPlaylist()
{ {
return GetPlaylistInternal(); return GetPlaylistInternal();
} }
@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
_sortedPlaylist = CreateQueueItemsFromArray(items); _sortedPlaylist = CreateQueueItemsFromArray(items);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{ {
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.Shuffle(); _shuffledPlaylist.Shuffle();
} }
@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{ {
if (PlayingItemIndex == NoPlayingItemIndex) if (PlayingItemIndex == NoPlayingItemIndex)
{ {
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.Shuffle(); _shuffledPlaylist.Shuffle();
} }
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted)) else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
{ {
// First time shuffle. // First time shuffle.
var playingItem = _sortedPlaylist[PlayingItemIndex]; var playingItem = _sortedPlaylist[PlayingItemIndex];
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist); _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.RemoveAt(PlayingItemIndex); _shuffledPlaylist.RemoveAt(PlayingItemIndex);
_shuffledPlaylist.Shuffle(); _shuffledPlaylist.Shuffle();
_shuffledPlaylist.Insert(0, playingItem); _shuffledPlaylist.Insert(0, playingItem);
@ -407,7 +407,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the next item in the playlist considering repeat mode and shuffle mode. /// Gets the next item in the playlist considering repeat mode and shuffle mode.
/// </summary> /// </summary>
/// <returns>The next item in the playlist.</returns> /// <returns>The next item in the playlist.</returns>
public QueueItem GetNextItemPlaylistId() public SyncPlayQueueItem GetNextItemPlaylistId()
{ {
int newIndex; int newIndex;
var playlist = GetPlaylistInternal(); var playlist = GetPlaylistInternal();
@ -502,12 +502,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Creates a list from the array of items. Each item is given an unique playlist identifier. /// Creates a list from the array of items. Each item is given an unique playlist identifier.
/// </summary> /// </summary>
/// <returns>The list of queue items.</returns> /// <returns>The list of queue items.</returns>
private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items) private List<SyncPlayQueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
{ {
var list = new List<QueueItem>(); var list = new List<SyncPlayQueueItem>();
foreach (var item in items) foreach (var item in items)
{ {
var queueItem = new QueueItem(item); var queueItem = new SyncPlayQueueItem(item);
list.Add(queueItem); list.Add(queueItem);
} }
@ -518,7 +518,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playlist considering the shuffle mode. /// Gets the current playlist considering the shuffle mode.
/// </summary> /// </summary>
/// <returns>The playlist.</returns> /// <returns>The playlist.</returns>
private List<QueueItem> GetPlaylistInternal() private List<SyncPlayQueueItem> GetPlaylistInternal()
{ {
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle)) if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{ {
@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playing item, depending on the shuffle mode. /// Gets the current playing item, depending on the shuffle mode.
/// </summary> /// </summary>
/// <returns>The playing item.</returns> /// <returns>The playing item.</returns>
private QueueItem GetPlayingItem() private SyncPlayQueueItem GetPlayingItem()
{ {
if (PlayingItemIndex == NoPlayingItemIndex) if (PlayingItemIndex == NoPlayingItemIndex)
{ {

View File

@ -27,13 +27,13 @@ public class EncodingOptions
EnableTonemapping = false; EnableTonemapping = false;
EnableVppTonemapping = false; EnableVppTonemapping = false;
TonemappingAlgorithm = "bt2390"; TonemappingAlgorithm = "bt2390";
TonemappingMode = "auto";
TonemappingRange = "auto"; TonemappingRange = "auto";
TonemappingDesat = 0; TonemappingDesat = 0;
TonemappingThreshold = 0.8;
TonemappingPeak = 100; TonemappingPeak = 100;
TonemappingParam = 0; TonemappingParam = 0;
VppTonemappingBrightness = 0; VppTonemappingBrightness = 16;
VppTonemappingContrast = 1.2; VppTonemappingContrast = 1;
H264Crf = 23; H264Crf = 23;
H265Crf = 28; H265Crf = 28;
DeinterlaceDoubleRate = false; DeinterlaceDoubleRate = false;
@ -137,6 +137,11 @@ public class EncodingOptions
/// </summary> /// </summary>
public string TonemappingAlgorithm { get; set; } public string TonemappingAlgorithm { get; set; }
/// <summary>
/// Gets or sets the tone-mapping mode.
/// </summary>
public string TonemappingMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the tone-mapping range. /// Gets or sets the tone-mapping range.
/// </summary> /// </summary>
@ -147,11 +152,6 @@ public class EncodingOptions
/// </summary> /// </summary>
public double TonemappingDesat { get; set; } public double TonemappingDesat { get; set; }
/// <summary>
/// Gets or sets the tone-mapping threshold.
/// </summary>
public double TonemappingThreshold { get; set; }
/// <summary> /// <summary>
/// Gets or sets the tone-mapping peak. /// Gets or sets the tone-mapping peak.
/// </summary> /// </summary>

View File

@ -757,8 +757,8 @@ namespace MediaBrowser.Model.Dlna
if (options.AllowVideoStreamCopy) if (options.AllowVideoStreamCopy)
{ {
// prefer direct copy profile // prefer direct copy profile
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
@ -768,7 +768,7 @@ namespace MediaBrowser.Model.Dlna
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec)) if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
{ {
var videoCodec = transcodingProfile.VideoCodec; var videoCodec = videoStream?.Codec;
var container = transcodingProfile.Container; var container = transcodingProfile.Container;
var appliedVideoConditions = options.Profile.CodecProfiles var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video && .Where(i => i.Type == CodecType.Video &&
@ -905,7 +905,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video && .Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoCodec, container) && i.ContainsAnyCodec(videoStream?.Codec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
var isFirstAppliedCodecProfile = true; var isFirstAppliedCodecProfile = true;
foreach (var i in appliedVideoConditions) foreach (var i in appliedVideoConditions)
@ -937,7 +937,7 @@ namespace MediaBrowser.Model.Dlna
var appliedAudioConditions = options.Profile.CodecProfiles var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio && .Where(i => i.Type == CodecType.VideoAudio &&
i.ContainsAnyCodec(audioCodec, container) && i.ContainsAnyCodec(audioStream?.Codec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
isFirstAppliedCodecProfile = true; isFirstAppliedCodecProfile = true;
foreach (var codecProfile in appliedAudioConditions) foreach (var codecProfile in appliedAudioConditions)
@ -1176,7 +1176,8 @@ namespace MediaBrowser.Model.Dlna
profile, profile,
"VideoCodecProfile", "VideoCodecProfile",
profile.CodecProfiles profile.CodecProfiles
.Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) && .Where(codecProfile => codecProfile.Type == CodecType.Video &&
codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
!checkVideoConditions(codecProfile.ApplyConditions).Any()) !checkVideoConditions(codecProfile.ApplyConditions).Any())
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
@ -1585,7 +1586,8 @@ namespace MediaBrowser.Model.Dlna
bool? isSecondaryAudio) bool? isSecondaryAudio)
{ {
return codecProfiles return codecProfiles
.Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) && .Where(profile => profile.Type == CodecType.VideoAudio &&
profile.ContainsAnyCodec(codec, container) &&
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)))
.SelectMany(profile => profile.Conditions) .SelectMany(profile => profile.Conditions)
.Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)); .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
@ -1602,7 +1604,8 @@ namespace MediaBrowser.Model.Dlna
bool checkConditions) bool checkConditions)
{ {
var conditions = codecProfiles var conditions = codecProfiles
.Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) && .Where(profile => profile.Type == CodecType.Audio &&
profile.ContainsAnyCodec(codec, container) &&
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)))
.SelectMany(profile => profile.Conditions); .SelectMany(profile => profile.Conditions);

View File

@ -1,31 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Model.Net
{
/// <summary>
/// Class WebSocketMessage.
/// </summary>
/// <typeparam name="T">The type of the data.</typeparam>
public class WebSocketMessage<T>
{
/// <summary>
/// Gets or sets the type of the message.
/// </summary>
/// <value>The type of the message.</value>
public SessionMessageType MessageType { get; set; }
public Guid MessageId { get; set; }
public string ServerId { get; set; }
/// <summary>
/// Gets or sets the data.
/// </summary>
/// <value>The data.</value>
public T Data { get; set; }
}
}

View File

@ -1,24 +1,19 @@
using System; using System;
namespace MediaBrowser.Model.SyncPlay namespace MediaBrowser.Model.SyncPlay;
{
/// <summary> /// <summary>
/// Class GroupUpdate. /// Group update without data.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the data of the message.</typeparam> public abstract class GroupUpdate
public class GroupUpdate<T>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GroupUpdate{T}"/> class. /// Initializes a new instance of the <see cref="GroupUpdate"/> class.
/// </summary> /// </summary>
/// <param name="groupId">The group identifier.</param> /// <param name="groupId">The group identifier.</param>
/// <param name="type">The update type.</param> protected GroupUpdate(Guid groupId)
/// <param name="data">The update data.</param>
public GroupUpdate(Guid groupId, GroupUpdateType type, T data)
{ {
GroupId = groupId; GroupId = groupId;
Type = type;
Data = data;
} }
/// <summary> /// <summary>
@ -31,12 +26,5 @@ namespace MediaBrowser.Model.SyncPlay
/// Gets the update type. /// Gets the update type.
/// </summary> /// </summary>
/// <value>The update type.</value> /// <value>The update type.</value>
public GroupUpdateType Type { get; } public GroupUpdateType Type { get; init; }
/// <summary>
/// Gets the update data.
/// </summary>
/// <value>The update data.</value>
public T Data { get; }
}
} }

View File

@ -0,0 +1,31 @@
#pragma warning disable SA1649
using System;
namespace MediaBrowser.Model.SyncPlay;
/// <summary>
/// Class GroupUpdate.
/// </summary>
/// <typeparam name="T">The type of the data of the message.</typeparam>
public class GroupUpdate<T> : GroupUpdate
{
/// <summary>
/// Initializes a new instance of the <see cref="GroupUpdate{T}"/> class.
/// </summary>
/// <param name="groupId">The group identifier.</param>
/// <param name="type">The update type.</param>
/// <param name="data">The update data.</param>
public GroupUpdate(Guid groupId, GroupUpdateType type, T data)
: base(groupId)
{
Data = data;
Type = type;
}
/// <summary>
/// Gets the update data.
/// </summary>
/// <value>The update data.</value>
public T Data { get; }
}

View File

@ -19,7 +19,7 @@ namespace MediaBrowser.Model.SyncPlay
/// <param name="isPlaying">The playing item status.</param> /// <param name="isPlaying">The playing item status.</param>
/// <param name="shuffleMode">The shuffle mode.</param> /// <param name="shuffleMode">The shuffle mode.</param>
/// <param name="repeatMode">The repeat mode.</param> /// <param name="repeatMode">The repeat mode.</param>
public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode) public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<SyncPlayQueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
{ {
Reason = reason; Reason = reason;
LastUpdate = lastUpdate; LastUpdate = lastUpdate;
@ -47,7 +47,7 @@ namespace MediaBrowser.Model.SyncPlay
/// Gets the playlist. /// Gets the playlist.
/// </summary> /// </summary>
/// <value>The playlist.</value> /// <value>The playlist.</value>
public IReadOnlyList<QueueItem> Playlist { get; } public IReadOnlyList<SyncPlayQueueItem> Playlist { get; }
/// <summary> /// <summary>
/// Gets the playing item index in the playlist. /// Gets the playing item index in the playlist.

View File

@ -5,13 +5,13 @@ namespace MediaBrowser.Model.SyncPlay
/// <summary> /// <summary>
/// Class QueueItem. /// Class QueueItem.
/// </summary> /// </summary>
public class QueueItem public class SyncPlayQueueItem
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="QueueItem"/> class. /// Initializes a new instance of the <see cref="SyncPlayQueueItem"/> class.
/// </summary> /// </summary>
/// <param name="itemId">The item identifier.</param> /// <param name="itemId">The item identifier.</param>
public QueueItem(Guid itemId) public SyncPlayQueueItem(Guid itemId)
{ {
ItemId = itemId; ItemId = itemId;
} }

View File

@ -12,6 +12,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;

View File

@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.TV
RemoveObsoleteEpisodes(item); RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item); RemoveObsoleteSeasons(item);
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -67,6 +67,20 @@ namespace MediaBrowser.Providers.TV
var sourceItem = source.Item; var sourceItem = source.Item;
var targetItem = target.Item; var targetItem = target.Item;
var sourceSeasonNames = sourceItem.SeasonNames;
var targetSeasonNames = targetItem.SeasonNames;
if (replaceData || targetSeasonNames.Count == 0)
{
targetItem.SeasonNames = sourceSeasonNames;
}
else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey))
{
foreach (var (number, name) in sourceSeasonNames)
{
targetSeasonNames.TryAdd(number, name);
}
}
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
{ {
@ -86,7 +100,7 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteSeasons(Series series) private void RemoveObsoleteSeasons(Series series)
{ {
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync. // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
var physicalSeasonNumbers = new HashSet<int>(); var physicalSeasonNumbers = new HashSet<int>();
var virtualSeasons = new List<Season>(); var virtualSeasons = new List<Season>();
foreach (var existingSeason in series.Children.OfType<Season>()) foreach (var existingSeason in series.Children.OfType<Season>())
@ -177,36 +191,43 @@ namespace MediaBrowser.Providers.TV
} }
/// <summary> /// <summary>
/// Creates seasons for all episodes that aren't in a season folder. /// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created. /// If no season number can be determined, a dummy season will be created.
/// Updates seasons names.
/// </summary> /// </summary>
/// <param name="series">The series.</param> /// <param name="series">The series.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns> /// <returns>The async task.</returns>
private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken)
{ {
var seasonNames = series.SeasonNames;
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var episodesInSeriesFolder = seriesChildren var seasons = seriesChildren.OfType<Season>().ToList();
var uniqueSeasonNumbers = seriesChildren
.OfType<Episode>() .OfType<Episode>()
.Where(i => !i.IsInSeasonFolder); .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct();
List<Season> seasons = seriesChildren.OfType<Season>().ToList();
// Loop through the unique season numbers // Loop through the unique season numbers
foreach (var episode in episodesInSeriesFolder) foreach (var seasonNumber in uniqueSeasonNumbers)
{ {
// Null season numbers will have a 'dummy' season created because seasons are always required. // Null season numbers will have a 'dummy' season created because seasons are always required.
var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
string? seasonName = null;
if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp))
{
seasonName = tmp;
}
if (existingSeason is null) if (existingSeason is null)
{ {
var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false); var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
seasons.Add(season); series.AddChild(season);
} }
else if (existingSeason.IsVirtualItem) else
{ {
existingSeason.IsVirtualItem = false; existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
} }
} }
@ -216,21 +237,17 @@ namespace MediaBrowser.Providers.TV
/// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
/// </summary> /// </summary>
/// <param name="series">The series.</param> /// <param name="series">The series.</param>
/// <param name="seasonName">The season name.</param>
/// <param name="seasonNumber">The season number.</param> /// <param name="seasonNumber">The season number.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly created season.</returns> /// <returns>The newly created season.</returns>
private async Task<Season> CreateSeasonAsync( private async Task<Season> CreateSeasonAsync(
Series series, Series series,
string? seasonName,
int? seasonNumber, int? seasonNumber,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
string seasonName = seasonNumber switch seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
{
null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
_ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
};
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
var season = new Season var season = new Season
@ -251,5 +268,20 @@ namespace MediaBrowser.Providers.TV
return season; return season;
} }
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
{
if (string.IsNullOrEmpty(seasonName))
{
seasonName = seasonNumber switch
{
null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
_ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
};
}
return seasonName;
}
} }
} }

View File

@ -55,6 +55,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break; break;
} }
case "seasonname":
{
var name = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(name))
{
item.Name = name;
}
break;
}
default: default:
base.FetchDataFromXmlNode(reader, itemResult); base.FetchDataFromXmlNode(reader, itemResult);
break; break;

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml; using System.Xml;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -110,6 +112,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break; break;
} }
case "namedseason":
{
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
var name = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(name) && parsed)
{
item.SeasonNames[seasonNumber] = name;
}
break;
}
default: default:
base.FetchDataFromXmlNode(reader, itemResult); base.FetchDataFromXmlNode(reader, itemResult);
break; break;

View File

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
using Xunit; using Xunit;
@ -73,5 +74,47 @@ namespace Jellyfin.Server.Implementations.Tests.Library
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result)); Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
Assert.Null(result); Assert.Null(result);
} }
[Theory]
[InlineData(null, '/', null)]
[InlineData(null, '\\', null)]
[InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
[InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
[InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
[InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
[InlineData("", '/', "")]
public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath)
{
Assert.Equal(expectedPath, path.NormalizePath(separator));
}
[Theory]
[InlineData("/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\Jeff\\myfile.mkv")]
[InlineData("\\home/jeff\\myfile.mkv")]
public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path)
{
var separator = Path.DirectorySeparatorChar;
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), path.NormalizePath());
}
[Theory]
[InlineData("/home/jeff/myfile.mkv", '/')]
[InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')]
[InlineData("\\home/jeff\\myfile.mkv", '/')]
public void NormalizePath_OutVar_Correct(string path, char expectedSeparator)
{
var result = path.NormalizePath(out var separator);
Assert.Equal(expectedSeparator, separator);
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), result);
}
[Fact]
public void NormalizePath_SpecifyInvalidSeparator_ThrowsException()
{
Assert.Throws<ArgumentException>(() => string.Empty.NormalizePath('a'));
}
} }
} }

View File

@ -1,7 +1,16 @@
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using AutoFixture;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.Plugins; using Emby.Server.Implementations.Plugins;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
@ -11,6 +20,21 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
{ {
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
private string _tempPath = string.Empty;
private string _pluginPath = string.Empty;
private JsonSerializerOptions _options;
public PluginManagerTests()
{
(_tempPath, _pluginPath) = GetTestPaths("plugin-" + Path.GetRandomFileName());
Directory.CreateDirectory(_pluginPath);
_options = GetTestSerializerOptions();
}
[Fact] [Fact]
public void SaveManifest_RoundTrip_Success() public void SaveManifest_RoundTrip_Success()
{ {
@ -20,12 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Version = "1.0" Version = "1.0"
}; };
var tempPath = Path.Combine(_testPathRoot, "manifest-" + Path.GetRandomFileName()); Assert.True(pluginManager.SaveManifest(manifest, _pluginPath));
Directory.CreateDirectory(tempPath);
Assert.True(pluginManager.SaveManifest(manifest, tempPath)); var res = pluginManager.LoadManifest(_pluginPath);
var res = pluginManager.LoadManifest(tempPath);
Assert.Equal(manifest.Category, res.Manifest.Category); Assert.Equal(manifest.Category, res.Manifest.Category);
Assert.Equal(manifest.Changelog, res.Manifest.Changelog); Assert.Equal(manifest.Changelog, res.Manifest.Changelog);
@ -40,6 +61,278 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Assert.Equal(manifest.Status, res.Manifest.Status); Assert.Equal(manifest.Status, res.Manifest.Status);
Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate); Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate);
Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath); Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath);
Assert.Equal(manifest.Assemblies, res.Manifest.Assemblies);
}
/// <summary>
/// Tests safe traversal within the plugin directory.
/// </summary>
/// <param name="dllFile">The safe path to evaluate.</param>
[Theory]
[InlineData("./some.dll")]
[InlineData("some.dll")]
[InlineData("sub/path/some.dll")]
public void Constructor_DiscoversSafePluginAssembly_Status_Active(string dllFile)
{
var manifest = new PluginManifest
{
Id = Guid.NewGuid(),
Name = "Safe Assembly",
Assemblies = new string[] { dllFile }
};
var filename = Path.GetFileName(dllFile)!;
var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!;
Directory.CreateDirectory(dllPath);
File.Create(Path.Combine(dllPath, filename));
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
var expectedFullPath = Path.Combine(_pluginPath, dllFile).Canonicalize();
Assert.NotNull(res);
Assert.NotEmpty(pluginManager.Plugins);
Assert.Equal(PluginStatus.Active, res!.Status);
Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]);
Assert.StartsWith(_pluginPath, expectedFullPath, StringComparison.InvariantCulture);
}
/// <summary>
/// Tests unsafe attempts to traverse to higher directories.
/// </summary>
/// <remarks>
/// Attempts to load directories outside of the plugin should be
/// constrained. Path traversal, shell expansion, and double encoding
/// can be used to load unintended files.
/// See <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more.
/// </remarks>
/// <param name="unsafePath">The unsafe path to evaluate.</param>
[Theory]
[InlineData("/some.dll")] // Root path.
[InlineData("../some.dll")] // Simple traversal.
[InlineData("C:\\some.dll")] // Windows root path.
[InlineData("test.txt")] // Not a DLL
[InlineData(".././.././../some.dll")] // Traversal with current and parent
[InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent
[InlineData("\\\\network\\resource.dll")] // UNC Path
[InlineData("https://jellyfin.org/some.dll")] // URL
[InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character.
public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath)
{
var manifest = new PluginManifest
{
Id = Guid.NewGuid(),
Name = "Unsafe Assembly",
Assemblies = new string[] { unsafePath }
};
// Only create very specific files. Otherwise the test will be exploiting path traversal.
var files = new string[]
{
"../other.dll",
"some.dll"
};
foreach (var file in files)
{
File.Create(Path.Combine(_pluginPath, file));
}
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
Assert.NotNull(res);
Assert.Empty(pluginManager.Plugins);
Assert.Equal(PluginStatus.Malfunctioned, res!.Status);
}
[Fact]
public async Task PopulateManifest_ExistingMetafilePlugin_PopulatesMissingFields()
{
var packageInfo = GenerateTestPackage();
// Partial plugin without a name, but matching version and package ID
var partial = new PluginManifest
{
Id = packageInfo.Id,
AutoUpdate = false, // Turn off AutoUpdate
Status = PluginStatus.Restart,
Version = new Version(1, 0, 0).ToString(),
Assemblies = new[] { "Jellyfin.Test.dll" }
};
var expectedManifest = new PluginManifest
{
Id = partial.Id,
Name = packageInfo.Name,
AutoUpdate = partial.AutoUpdate,
Status = PluginStatus.Active,
Owner = packageInfo.Owner,
Assemblies = partial.Assemblies,
Category = packageInfo.Category,
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
Changelog = packageInfo.Versions[0].Changelog!,
Version = new Version(1, 0).ToString(),
ImagePath = string.Empty
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = File.ReadAllBytes(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equivalent(expectedManifest, result);
}
[Fact]
public async Task PopulateManifest_NoMetafile_PreservesManifest()
{
var packageInfo = GenerateTestPackage();
var expectedManifest = new PluginManifest
{
Id = packageInfo.Id,
Name = packageInfo.Name,
AutoUpdate = true,
Status = PluginStatus.Active,
Owner = packageInfo.Owner,
Assemblies = Array.Empty<string>(),
Category = packageInfo.Category,
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
Changelog = packageInfo.Versions[0].Changelog!,
Version = packageInfo.Versions[0].Version,
ImagePath = string.Empty
};
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var metafilePath = Path.Combine(_pluginPath, "meta.json");
var resultBytes = File.ReadAllBytes(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equivalent(expectedManifest, result);
}
[Fact]
public async Task PopulateManifest_ExistingMetafileMismatchedIds_Status_Malfunctioned()
{
var packageInfo = GenerateTestPackage();
// Partial plugin without a name, but matching version and package ID
var partial = new PluginManifest
{
Id = Guid.NewGuid(),
Version = new Version(1, 0, 0).ToString()
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = File.ReadAllBytes(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equal(packageInfo.Name, result.Name);
Assert.Equal(PluginStatus.Malfunctioned, result.Status);
}
[Fact]
public async Task PopulateManifest_ExistingMetafileMismatchedVersions_Updates_Version()
{
var packageInfo = GenerateTestPackage();
var partial = new PluginManifest
{
Id = packageInfo.Id,
Version = new Version(2, 0, 0).ToString()
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = File.ReadAllBytes(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equal(packageInfo.Name, result.Name);
Assert.Equal(PluginStatus.Active, result.Status);
Assert.Equal(packageInfo.Versions[0].Version, result.Version);
}
private PackageInfo GenerateTestPackage()
{
var fixture = new Fixture();
fixture.Customize<PackageInfo>(c => c.Without(x => x.Versions).Without(x => x.ImageUrl));
fixture.Customize<VersionInfo>(c => c.Without(x => x.Version).Without(x => x.Timestamp));
var versionInfo = fixture.Create<VersionInfo>();
versionInfo.Version = new Version(1, 0).ToString();
versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
var packageInfo = fixture.Create<PackageInfo>();
packageInfo.Versions = new[] { versionInfo };
return packageInfo;
}
private JsonSerializerOptions GetTestSerializerOptions()
{
var options = new JsonSerializerOptions(JsonDefaults.Options)
{
WriteIndented = true
};
for (var i = 0; i < options.Converters.Count; i++)
{
// Remove the Guid converter for parity with plugin manager.
if (options.Converters[i] is JsonGuidConverter converter)
{
options.Converters.Remove(converter);
}
}
return options;
}
private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName)
{
var tempPath = Path.Combine(_testPathRoot, "plugin-manager" + Path.GetRandomFileName());
var pluginPath = Path.Combine(tempPath, pluginFolderName);
return (tempPath, pluginPath);
} }
} }
} }