mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 09:59:06 -07:00
Merge branch 'master' into network-rewrite
This commit is contained in:
commit
32499f0e98
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
|
14
.github/workflows/commands.yml
vendored
14
.github/workflows/commands.yml
vendored
@ -17,14 +17,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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 }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
@ -51,14 +51,14 @@ jobs:
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Notify as 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 }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
@ -93,7 +93,7 @@ jobs:
|
||||
exit ${retcode}
|
||||
|
||||
- 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() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
@ -108,7 +108,7 @@ jobs:
|
||||
reactions: hooray
|
||||
|
||||
- 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() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
8
.github/workflows/openapi.yml
vendored
8
.github/workflows/openapi.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -110,7 +110,7 @@ jobs:
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- 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 != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
</details>
|
||||
- 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 != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
@ -165,6 +165,7 @@
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [ipitio](https://github.com/ipitio)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
@ -63,7 +63,7 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.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="SharpFuzz" Version="2.0.2" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||
|
@ -49,20 +49,24 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
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();
|
||||
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
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
ms,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// 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 (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
@ -70,7 +74,7 @@ namespace Emby.Dlna.PlayTo
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
var xmlReader = new StringReader(xmlString);
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Providers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return false;
|
||||
}
|
||||
|
||||
char oldDirectorySeparatorChar;
|
||||
char 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);
|
||||
subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
|
||||
path = path.NormalizePath(newDirectorySeparatorChar);
|
||||
|
||||
// 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
|
||||
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
if (season.IndexNumber.HasValue)
|
||||
{
|
||||
var seasonNumber = season.IndexNumber.Value;
|
||||
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
if (string.IsNullOrEmpty(season.Name))
|
||||
{
|
||||
var seasonNames = series.SeasonNames;
|
||||
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
||||
{
|
||||
season.Name = seasonName;
|
||||
}
|
||||
else
|
||||
{
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return season;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -9,6 +10,8 @@ using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
using MediaBrowser.Common;
|
||||
@ -29,6 +32,8 @@ namespace Emby.Server.Implementations.Plugins
|
||||
/// </summary>
|
||||
public class PluginManager : IPluginManager
|
||||
{
|
||||
private const string MetafileName = "meta.json";
|
||||
|
||||
private readonly string _pluginsPath;
|
||||
private readonly Version _appVersion;
|
||||
private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
|
||||
@ -44,7 +49,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
||||
/// </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="config">The <see cref="ServerConfiguration"/>.</param>
|
||||
/// <param name="pluginsPath">The plugin path.</param>
|
||||
@ -371,7 +376,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||
File.WriteAllText(Path.Combine(path, "meta.json"), data);
|
||||
File.WriteAllText(Path.Combine(path, MetafileName), data);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
@ -382,7 +387,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
}
|
||||
|
||||
/// <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 imagePath = string.Empty;
|
||||
@ -427,9 +432,71 @@ namespace Emby.Server.Implementations.Plugins
|
||||
ImagePath = imagePath
|
||||
};
|
||||
|
||||
if (!await ReconcileManifest(manifest, path))
|
||||
{
|
||||
// An error occurred during reconciliation and saving could be undesirable.
|
||||
return false;
|
||||
}
|
||||
|
||||
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>
|
||||
/// Changes a plugin's load status.
|
||||
/// </summary>
|
||||
@ -594,7 +661,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
{
|
||||
Version? version;
|
||||
PluginManifest? manifest = null;
|
||||
var metafile = Path.Combine(dir, "meta.json");
|
||||
var metafile = Path.Combine(dir, MetafileName);
|
||||
if (File.Exists(metafile))
|
||||
{
|
||||
// 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];
|
||||
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)
|
||||
{
|
||||
lastName = entry.Name;
|
||||
@ -734,6 +809,68 @@ namespace Emby.Server.Implementations.Plugins
|
||||
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>
|
||||
/// Changes the status of the other versions of the plugin to "Superceded".
|
||||
/// </summary>
|
||||
|
@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
|
||||
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.
|
||||
@ -555,7 +555,10 @@ namespace Emby.Server.Implementations.Updates
|
||||
stream.Position = 0;
|
||||
using var reader = new ZipArchive(stream);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
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.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.OpenApi.Any;
|
||||
@ -36,17 +40,141 @@ namespace Jellyfin.Server.Filters
|
||||
/// <inheritdoc />
|
||||
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(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);
|
||||
|
||||
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
||||
|
@ -57,7 +57,7 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// <param name="path">The path where to save the manifest.</param>
|
||||
/// <param name="status">Initial status of the plugin.</param>
|
||||
/// <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>
|
||||
/// Imports plugin details from a folder.
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
@ -23,6 +24,7 @@ namespace MediaBrowser.Common.Plugins
|
||||
Overview = string.Empty;
|
||||
TargetAbi = string.Empty;
|
||||
Version = string.Empty;
|
||||
Assemblies = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -104,5 +106,12 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// </summary>
|
||||
[JsonPropertyName("imagePath")]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -28,12 +28,16 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
public Series()
|
||||
{
|
||||
AirDays = Array.Empty<DayOfWeek>();
|
||||
SeasonNames = new Dictionary<int, string>();
|
||||
}
|
||||
|
||||
public DayOfWeek[] AirDays { get; set; }
|
||||
|
||||
public string AirTime { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<int, string> SeasonNames { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
|
@ -45,6 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
private readonly Version _minFFmpegImplictHwaccel = 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[]
|
||||
{
|
||||
@ -162,7 +163,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
private bool IsVaapiFullSupported()
|
||||
{
|
||||
return _mediaEncoder.SupportsHwaccel("vaapi")
|
||||
return _mediaEncoder.SupportsHwaccel("drm")
|
||||
&& _mediaEncoder.SupportsHwaccel("vaapi")
|
||||
&& _mediaEncoder.SupportsFilter("scale_vaapi")
|
||||
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
|
||||
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
|
||||
@ -712,28 +714,43 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
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;
|
||||
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
|
||||
var options = string.IsNullOrEmpty(driver)
|
||||
? renderNodePath
|
||||
: ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
|
||||
var driverOpts = string.IsNullOrEmpty(driver)
|
||||
? ":" + renderNodePath
|
||||
: ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
|
||||
var options = string.IsNullOrEmpty(srcDeviceAlias)
|
||||
? driverOpts
|
||||
: "@" + srcDeviceAlias;
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -init_hw_device vaapi={0}:{1}",
|
||||
" -init_hw_device vaapi={0}{1}",
|
||||
alias,
|
||||
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)
|
||||
{
|
||||
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// 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())
|
||||
@ -754,9 +771,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
|
||||
{
|
||||
// DVBSUB and DVDSUB use the fixed canvas size 720x576
|
||||
if (state.SubtitleStream is not null
|
||||
&& 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 inH = state.VideoStream?.Height;
|
||||
@ -824,21 +844,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||
{
|
||||
args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias));
|
||||
args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
|
||||
}
|
||||
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
||||
{
|
||||
// Only override i965 since it has lower priority than iHD in libva lookup.
|
||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
|
||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
|
||||
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
|
||||
args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
|
||||
}
|
||||
|
||||
var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
|
||||
var filterDevArgs = string.Empty;
|
||||
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
|
||||
|
||||
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
|
||||
@ -855,15 +871,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|
||||
&& 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.
|
||||
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
|
||||
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
|
||||
}
|
||||
else if (doOclTonemap)
|
||||
else
|
||||
{
|
||||
// ROCm/ROCr OpenCL runtime
|
||||
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
|
||||
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
|
||||
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias));
|
||||
filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
|
||||
|
||||
if (doOclTonemap)
|
||||
{
|
||||
// ROCm/ROCr OpenCL runtime
|
||||
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
|
||||
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (doOclTonemap)
|
||||
@ -1549,11 +1574,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
param += " -preset p7";
|
||||
break;
|
||||
|
||||
case "slow":
|
||||
case "slower":
|
||||
param += " -preset p6";
|
||||
break;
|
||||
|
||||
case "slower":
|
||||
case "slow":
|
||||
param += " -preset p5";
|
||||
break;
|
||||
|
||||
@ -1586,8 +1611,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
switch (encodingOptions.EncoderPreset)
|
||||
{
|
||||
case "veryslow":
|
||||
case "slow":
|
||||
case "slower":
|
||||
case "slow":
|
||||
param += " -quality quality";
|
||||
break;
|
||||
|
||||
@ -2929,7 +2954,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
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))
|
||||
{
|
||||
@ -2941,7 +2966,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
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(
|
||||
CultureInfo.InvariantCulture,
|
||||
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}";
|
||||
|
||||
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,
|
||||
options.TonemappingPeak,
|
||||
options.TonemappingDesat,
|
||||
options.TonemappingMode,
|
||||
options.TonemappingParam,
|
||||
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>
|
||||
/// Gets the parameter of software filter chain.
|
||||
/// </summary>
|
||||
@ -4224,7 +4330,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
|
||||
var isSwEncoder = !isVaapiEncoder;
|
||||
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
|
||||
|
||||
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
||||
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
||||
@ -4253,99 +4358,81 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
mainFilters.Add(swDeintFilter);
|
||||
}
|
||||
|
||||
var outFormat = doVkTonemap ? "yuv420p10le" : "nv12";
|
||||
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)
|
||||
if (doVkTonemap || hasSubs)
|
||||
{
|
||||
mainFilters.Add("hwupload=derive_device=vaapi");
|
||||
mainFilters.Add("format=vaapi");
|
||||
mainFilters.Add("hwmap=derive_device=vulkan");
|
||||
// sw => hw
|
||||
mainFilters.Add("hwupload=derive_device=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)
|
||||
{
|
||||
// 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
|
||||
if (doDeintH2645)
|
||||
{
|
||||
var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
|
||||
mainFilters.Add(deintFilter);
|
||||
}
|
||||
|
||||
// hw scale
|
||||
var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
}
|
||||
|
||||
// vk libplacebo
|
||||
if (doVkTonemap || hasSubs)
|
||||
{
|
||||
var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
mainFilters.Add(libplaceboFilter);
|
||||
}
|
||||
|
||||
if (doVkTonemap && !hasSubs)
|
||||
{
|
||||
// OUTPUT vaapi(nv12) surface(vram)
|
||||
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
|
||||
mainFilters.Add("hwmap=derive_device=drm");
|
||||
mainFilters.Add("format=drm_prime");
|
||||
mainFilters.Add("hwmap=derive_device=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 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
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
|
||||
if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
|
||||
if (!hasSubs)
|
||||
{
|
||||
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
|
||||
mainFilters.Add("hwmap=derive_device=vulkan");
|
||||
mainFilters.Add("format=vulkan");
|
||||
}
|
||||
|
||||
// vk tonemap
|
||||
if (doVkTonemap)
|
||||
{
|
||||
var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12";
|
||||
var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat);
|
||||
mainFilters.Add(tonemapFilter);
|
||||
}
|
||||
|
||||
if (doVkTonemap && isVaInVaOut && !hasSubs)
|
||||
{
|
||||
// OUTPUT vaapi(nv12/bgra) surface(vram)
|
||||
// reverse-mapping via vaapi-vulkan interop.
|
||||
mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
|
||||
mainFilters.Add("format=vaapi");
|
||||
}
|
||||
|
||||
var memoryOutput = false;
|
||||
var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
|
||||
if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
|
||||
{
|
||||
memoryOutput = true;
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
if (isSwDecoder && isVaapiEncoder)
|
||||
{
|
||||
memoryOutput = true;
|
||||
}
|
||||
|
||||
if (memoryOutput)
|
||||
{
|
||||
// text subtitles
|
||||
if (hasTextSubs)
|
||||
if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
|
||||
{
|
||||
var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
|
||||
mainFilters.Add(textSubtitlesFilter);
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryOutput && isVaapiEncoder)
|
||||
{
|
||||
if (!hasGraphicalSubs)
|
||||
if (isSwDecoder && isVaapiEncoder && !doVkTonemap)
|
||||
{
|
||||
mainFilters.Add("hwupload_vaapi");
|
||||
}
|
||||
@ -4354,55 +4441,53 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/* Make sub and overlay filters for subtitle stream */
|
||||
var subFilters = new List<string>();
|
||||
var overlayFilters = new List<string>();
|
||||
if (isVaInVaOut)
|
||||
{
|
||||
if (hasSubs)
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale=s=1280x720,format=bgra,hwupload
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
subFilters.Add(subTextSubtitlesFilter);
|
||||
}
|
||||
|
||||
// prefer vaapi hwupload to vulkan hwupload,
|
||||
// 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");
|
||||
|
||||
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
|
||||
|
||||
// TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi.
|
||||
overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none");
|
||||
|
||||
// OUTPUT vaapi(nv12/bgra) surface(vram)
|
||||
// reverse-mapping via vaapi-vulkan interop.
|
||||
overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1");
|
||||
overlayFilters.Add("format=vaapi");
|
||||
}
|
||||
}
|
||||
else if (memoryOutput)
|
||||
if (hasSubs)
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale=s=1280x720,format=bgra,hwupload
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
subFilters.Add(subTextSubtitlesFilter);
|
||||
}
|
||||
|
||||
if (isVaapiEncoder)
|
||||
subFilters.Add("hwupload=derive_device=vulkan");
|
||||
subFilters.Add("format=vulkan");
|
||||
|
||||
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
|
||||
|
||||
if (isSwEncoder)
|
||||
{
|
||||
// OUTPUT nv12 surface(memory)
|
||||
overlayFilters.Add("scale_vulkan=format=nv12");
|
||||
overlayFilters.Add("hwdownload");
|
||||
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");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
MediaBrowser.Controller/Net/WebSocketMessage.cs
Normal file
28
MediaBrowser.Controller/Net/WebSocketMessage.cs
Normal 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; }
|
||||
}
|
33
MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
Normal file
33
MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
Normal 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; }
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
||||
/// The sorted playlist.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// The shuffled playlist.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>The playlist.</returns>
|
||||
public IReadOnlyList<QueueItem> GetPlaylist()
|
||||
public IReadOnlyList<SyncPlayQueueItem> GetPlaylist()
|
||||
{
|
||||
return GetPlaylistInternal();
|
||||
}
|
||||
@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
||||
_sortedPlaylist = CreateQueueItemsFromArray(items);
|
||||
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
||||
{
|
||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist.Shuffle();
|
||||
}
|
||||
|
||||
@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
||||
{
|
||||
if (PlayingItemIndex == NoPlayingItemIndex)
|
||||
{
|
||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist.Shuffle();
|
||||
}
|
||||
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
|
||||
{
|
||||
// First time shuffle.
|
||||
var playingItem = _sortedPlaylist[PlayingItemIndex];
|
||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||
_shuffledPlaylist.RemoveAt(PlayingItemIndex);
|
||||
_shuffledPlaylist.Shuffle();
|
||||
_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.
|
||||
/// </summary>
|
||||
/// <returns>The next item in the playlist.</returns>
|
||||
public QueueItem GetNextItemPlaylistId()
|
||||
public SyncPlayQueueItem GetNextItemPlaylistId()
|
||||
{
|
||||
int newIndex;
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
var queueItem = new QueueItem(item);
|
||||
var queueItem = new SyncPlayQueueItem(item);
|
||||
list.Add(queueItem);
|
||||
}
|
||||
|
||||
@ -518,7 +518,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
||||
/// Gets the current playlist considering the shuffle mode.
|
||||
/// </summary>
|
||||
/// <returns>The playlist.</returns>
|
||||
private List<QueueItem> GetPlaylistInternal()
|
||||
private List<SyncPlayQueueItem> GetPlaylistInternal()
|
||||
{
|
||||
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
||||
{
|
||||
@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
||||
/// Gets the current playing item, depending on the shuffle mode.
|
||||
/// </summary>
|
||||
/// <returns>The playing item.</returns>
|
||||
private QueueItem GetPlayingItem()
|
||||
private SyncPlayQueueItem GetPlayingItem()
|
||||
{
|
||||
if (PlayingItemIndex == NoPlayingItemIndex)
|
||||
{
|
||||
|
@ -27,13 +27,13 @@ public class EncodingOptions
|
||||
EnableTonemapping = false;
|
||||
EnableVppTonemapping = false;
|
||||
TonemappingAlgorithm = "bt2390";
|
||||
TonemappingMode = "auto";
|
||||
TonemappingRange = "auto";
|
||||
TonemappingDesat = 0;
|
||||
TonemappingThreshold = 0.8;
|
||||
TonemappingPeak = 100;
|
||||
TonemappingParam = 0;
|
||||
VppTonemappingBrightness = 0;
|
||||
VppTonemappingContrast = 1.2;
|
||||
VppTonemappingBrightness = 16;
|
||||
VppTonemappingContrast = 1;
|
||||
H264Crf = 23;
|
||||
H265Crf = 28;
|
||||
DeinterlaceDoubleRate = false;
|
||||
@ -137,6 +137,11 @@ public class EncodingOptions
|
||||
/// </summary>
|
||||
public string TonemappingAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tone-mapping mode.
|
||||
/// </summary>
|
||||
public string TonemappingMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tone-mapping range.
|
||||
/// </summary>
|
||||
@ -147,11 +152,6 @@ public class EncodingOptions
|
||||
/// </summary>
|
||||
public double TonemappingDesat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tone-mapping threshold.
|
||||
/// </summary>
|
||||
public double TonemappingThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tone-mapping peak.
|
||||
/// </summary>
|
||||
|
@ -757,8 +757,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
if (options.AllowVideoStreamCopy)
|
||||
{
|
||||
// prefer direct copy profile
|
||||
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
||||
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
|
||||
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
|
||||
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
|
||||
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
||||
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
||||
|
||||
@ -768,7 +768,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
|
||||
{
|
||||
var videoCodec = transcodingProfile.VideoCodec;
|
||||
var videoCodec = videoStream?.Codec;
|
||||
var container = transcodingProfile.Container;
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
@ -905,7 +905,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.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)));
|
||||
var isFirstAppliedCodecProfile = true;
|
||||
foreach (var i in appliedVideoConditions)
|
||||
@ -937,7 +937,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||
.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)));
|
||||
isFirstAppliedCodecProfile = true;
|
||||
foreach (var codecProfile in appliedAudioConditions)
|
||||
@ -1176,7 +1176,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
profile,
|
||||
"VideoCodecProfile",
|
||||
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())
|
||||
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
|
||||
|
||||
@ -1585,7 +1586,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool? isSecondaryAudio)
|
||||
{
|
||||
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)))
|
||||
.SelectMany(profile => profile.Conditions)
|
||||
.Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
|
||||
@ -1602,7 +1604,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool checkConditions)
|
||||
{
|
||||
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)))
|
||||
.SelectMany(profile => profile.Conditions);
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,42 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.SyncPlay
|
||||
namespace MediaBrowser.Model.SyncPlay;
|
||||
|
||||
/// <summary>
|
||||
/// Group update without data.
|
||||
/// </summary>
|
||||
public abstract class GroupUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GroupUpdate.
|
||||
/// Initializes a new instance of the <see cref="GroupUpdate"/> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the data of the message.</typeparam>
|
||||
public class GroupUpdate<T>
|
||||
/// <param name="groupId">The group identifier.</param>
|
||||
protected GroupUpdate(Guid groupId)
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
GroupId = groupId;
|
||||
Type = type;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group identifier.
|
||||
/// </summary>
|
||||
/// <value>The group identifier.</value>
|
||||
public Guid GroupId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the update type.
|
||||
/// </summary>
|
||||
/// <value>The update type.</value>
|
||||
public GroupUpdateType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the update data.
|
||||
/// </summary>
|
||||
/// <value>The update data.</value>
|
||||
public T Data { get; }
|
||||
GroupId = groupId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group identifier.
|
||||
/// </summary>
|
||||
/// <value>The group identifier.</value>
|
||||
public Guid GroupId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the update type.
|
||||
/// </summary>
|
||||
/// <value>The update type.</value>
|
||||
public GroupUpdateType Type { get; init; }
|
||||
}
|
||||
|
31
MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs
Normal file
31
MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs
Normal 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; }
|
||||
}
|
@ -19,7 +19,7 @@ namespace MediaBrowser.Model.SyncPlay
|
||||
/// <param name="isPlaying">The playing item status.</param>
|
||||
/// <param name="shuffleMode">The shuffle 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;
|
||||
LastUpdate = lastUpdate;
|
||||
@ -47,7 +47,7 @@ namespace MediaBrowser.Model.SyncPlay
|
||||
/// Gets the playlist.
|
||||
/// </summary>
|
||||
/// <value>The playlist.</value>
|
||||
public IReadOnlyList<QueueItem> Playlist { get; }
|
||||
public IReadOnlyList<SyncPlayQueueItem> Playlist { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playing item index in the playlist.
|
||||
|
@ -5,13 +5,13 @@ namespace MediaBrowser.Model.SyncPlay
|
||||
/// <summary>
|
||||
/// Class QueueItem.
|
||||
/// </summary>
|
||||
public class QueueItem
|
||||
public class SyncPlayQueueItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueueItem"/> class.
|
||||
/// Initializes a new instance of the <see cref="SyncPlayQueueItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item identifier.</param>
|
||||
public QueueItem(Guid itemId)
|
||||
public SyncPlayQueueItem(Guid itemId)
|
||||
{
|
||||
ItemId = itemId;
|
||||
}
|
@ -12,6 +12,7 @@ using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
RemoveObsoleteEpisodes(item);
|
||||
RemoveObsoleteSeasons(item);
|
||||
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -67,6 +67,20 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
var sourceItem = source.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))
|
||||
{
|
||||
@ -86,7 +100,7 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
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 virtualSeasons = new List<Season>();
|
||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||
@ -177,36 +191,43 @@ namespace MediaBrowser.Providers.TV
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Updates seasons names.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <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 episodesInSeriesFolder = seriesChildren
|
||||
var seasons = seriesChildren.OfType<Season>().ToList();
|
||||
var uniqueSeasonNumbers = seriesChildren
|
||||
.OfType<Episode>()
|
||||
.Where(i => !i.IsInSeasonFolder);
|
||||
|
||||
List<Season> seasons = seriesChildren.OfType<Season>().ToList();
|
||||
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
|
||||
.Distinct();
|
||||
|
||||
// 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.
|
||||
var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
|
||||
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)
|
||||
{
|
||||
var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
seasons.Add(season);
|
||||
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seasonName">The season name.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The newly created season.</returns>
|
||||
private async Task<Season> CreateSeasonAsync(
|
||||
Series series,
|
||||
string? seasonName,
|
||||
int? seasonNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string seasonName = seasonNumber switch
|
||||
{
|
||||
null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
|
||||
0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
|
||||
_ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
|
||||
};
|
||||
|
||||
seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
|
||||
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
|
||||
|
||||
var season = new Season
|
||||
@ -251,5 +268,20 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
break;
|
||||
}
|
||||
|
||||
case "seasonname":
|
||||
{
|
||||
var name = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
item.Name = name;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
@ -110,6 +112,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
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:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Xunit;
|
||||
|
||||
@ -73,5 +74,47 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
||||
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,16 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AutoFixture;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Emby.Server.Implementations.Plugins;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
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 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]
|
||||
public void SaveManifest_RoundTrip_Success()
|
||||
{
|
||||
@ -20,12 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
Version = "1.0"
|
||||
};
|
||||
|
||||
var tempPath = Path.Combine(_testPathRoot, "manifest-" + Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempPath);
|
||||
Assert.True(pluginManager.SaveManifest(manifest, _pluginPath));
|
||||
|
||||
Assert.True(pluginManager.SaveManifest(manifest, tempPath));
|
||||
|
||||
var res = pluginManager.LoadManifest(tempPath);
|
||||
var res = pluginManager.LoadManifest(_pluginPath);
|
||||
|
||||
Assert.Equal(manifest.Category, res.Manifest.Category);
|
||||
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.AutoUpdate, res.Manifest.AutoUpdate);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -681,4 +681,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user