mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
with:
|
with:
|
||||||
|
14
.github/workflows/commands.yml
vendored
14
.github/workflows/commands.yml
vendored
@ -17,14 +17,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ github.event.comment != null }}
|
if: ${{ github.event.comment != null }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -51,14 +51,14 @@ jobs:
|
|||||||
reactions: eyes
|
reactions: eyes
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Notify as running
|
- name: Notify as running
|
||||||
id: comment_running
|
id: comment_running
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ github.event.comment != null }}
|
if: ${{ github.event.comment != null }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -93,7 +93,7 @@ jobs:
|
|||||||
exit ${retcode}
|
exit ${retcode}
|
||||||
|
|
||||||
- name: Notify with result success
|
- name: Notify with result success
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ github.event.comment != null && success() }}
|
if: ${{ github.event.comment != null && success() }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -108,7 +108,7 @@ jobs:
|
|||||||
reactions: hooray
|
reactions: hooray
|
||||||
|
|
||||||
- name: Notify with result failure
|
- name: Notify with result failure
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ github.event.comment != null && failure() }}
|
if: ${{ github.event.comment != null && failure() }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
8
.github/workflows/openapi.yml
vendored
8
.github/workflows/openapi.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@ -39,7 +39,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@ -110,7 +110,7 @@ jobs:
|
|||||||
direction: last
|
direction: last
|
||||||
body-includes: openapi-diff-workflow-comment
|
body-includes: openapi-diff-workflow-comment
|
||||||
- name: Reply or edit difference comment (changed)
|
- name: Reply or edit difference comment (changed)
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
- name: Edit difference comment (unchanged)
|
- name: Edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
|
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
||||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
@ -165,6 +165,7 @@
|
|||||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||||
- [ipitio](https://github.com/ipitio)
|
- [ipitio](https://github.com/ipitio)
|
||||||
|
- [TheTyrius](https://github.com/TheTyrius)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.0" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.0.2" />
|
<PackageVersion Include="SharpFuzz" Version="2.0.2" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||||
|
@ -49,20 +49,24 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||||
|
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using MemoryStream ms = new MemoryStream();
|
||||||
|
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await XDocument.LoadAsync(
|
return await XDocument.LoadAsync(
|
||||||
stream,
|
ms,
|
||||||
LoadOptions.None,
|
LoadOptions.None,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
{
|
{
|
||||||
// try correcting the Xml response with common errors
|
// try correcting the Xml response with common errors
|
||||||
var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
ms.Position = 0;
|
||||||
|
using StreamReader sr = new StreamReader(ms);
|
||||||
|
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// find and replace unescaped ampersands (&)
|
// find and replace unescaped ampersands (&)
|
||||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||||
@ -70,7 +74,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// retry reading Xml
|
// retry reading Xml
|
||||||
var xmlReader = new StringReader(xmlString);
|
using var xmlReader = new StringReader(xmlString);
|
||||||
return await XDocument.LoadAsync(
|
return await XDocument.LoadAsync(
|
||||||
xmlReader,
|
xmlReader,
|
||||||
LoadOptions.None,
|
LoadOptions.None,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
using MediaBrowser.Common.Providers;
|
using MediaBrowser.Common.Providers;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char oldDirectorySeparatorChar;
|
subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
|
||||||
char newDirectorySeparatorChar;
|
path = path.NormalizePath(newDirectorySeparatorChar);
|
||||||
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
|
||||||
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
|
||||||
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
|
||||||
if (newSubPath.Contains('/', StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
oldDirectorySeparatorChar = '\\';
|
|
||||||
newDirectorySeparatorChar = '/';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
oldDirectorySeparatorChar = '/';
|
|
||||||
newDirectorySeparatorChar = '\\';
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
|
||||||
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
|
||||||
|
|
||||||
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
||||||
// when the sub path matches a similar but in-complete subpath
|
// when the sub path matches a similar but in-complete subpath
|
||||||
@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to canonicalize.</param>
|
||||||
|
/// <returns>The fully expanded, normalized path.</returns>
|
||||||
|
public static string Canonicalize(this string path)
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(path).NormalizePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to normalize.</param>
|
||||||
|
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||||
|
[return: NotNullIfNotNull(nameof(path))]
|
||||||
|
public static string? NormalizePath(this string? path)
|
||||||
|
{
|
||||||
|
return path.NormalizePath(Path.DirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalizes the path's directory separator character.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to normalize.</param>
|
||||||
|
/// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
|
||||||
|
/// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
|
||||||
|
[return: NotNullIfNotNull(nameof(path))]
|
||||||
|
public static string? NormalizePath(this string? path, out char separator)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
separator = default;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSeparator = '\\';
|
||||||
|
|
||||||
|
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||||
|
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||||
|
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||||
|
if (path.Contains('/', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
newSeparator = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
separator = newSeparator;
|
||||||
|
|
||||||
|
return path.NormalizePath(newSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalizes the path's directory separator character to the specified character.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to normalize.</param>
|
||||||
|
/// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
|
||||||
|
/// <returns>The normalized path.</returns>
|
||||||
|
/// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
|
||||||
|
[return: NotNullIfNotNull(nameof(path))]
|
||||||
|
public static string? NormalizePath(this string? path, char newSeparator)
|
||||||
|
{
|
||||||
|
const char Bs = '\\';
|
||||||
|
const char Fs = '/';
|
||||||
|
|
||||||
|
if (!(newSeparator == Bs || newSeparator == Fs))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("The character must be a directory separator.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
if (season.IndexNumber.HasValue)
|
if (season.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
var seasonNumber = season.IndexNumber.Value;
|
var seasonNumber = season.IndexNumber.Value;
|
||||||
|
if (string.IsNullOrEmpty(season.Name))
|
||||||
|
{
|
||||||
|
var seasonNames = series.SeasonNames;
|
||||||
|
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
|
||||||
|
{
|
||||||
|
season.Name = seasonName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
season.Name = seasonNumber == 0 ?
|
season.Name = seasonNumber == 0 ?
|
||||||
args.LibraryOptions.SeasonZeroDisplayName :
|
args.LibraryOptions.SeasonZeroDisplayName :
|
||||||
string.Format(
|
string.Format(
|
||||||
@ -90,6 +98,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
seasonNumber,
|
seasonNumber,
|
||||||
args.LibraryOptions.PreferredMetadataLanguage);
|
args.LibraryOptions.PreferredMetadataLanguage);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return season;
|
return season;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -9,6 +10,8 @@ using System.Runtime.Loader;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Server.Implementations.Library;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using Jellyfin.Extensions.Json.Converters;
|
using Jellyfin.Extensions.Json.Converters;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
@ -29,6 +32,8 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PluginManager : IPluginManager
|
public class PluginManager : IPluginManager
|
||||||
{
|
{
|
||||||
|
private const string MetafileName = "meta.json";
|
||||||
|
|
||||||
private readonly string _pluginsPath;
|
private readonly string _pluginsPath;
|
||||||
private readonly Version _appVersion;
|
private readonly Version _appVersion;
|
||||||
private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
|
private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
|
||||||
@ -44,7 +49,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
/// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param>
|
||||||
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
|
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
|
||||||
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
|
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
|
||||||
/// <param name="pluginsPath">The plugin path.</param>
|
/// <param name="pluginsPath">The plugin path.</param>
|
||||||
@ -371,7 +376,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||||
File.WriteAllText(Path.Combine(path, "meta.json"), data);
|
File.WriteAllText(Path.Combine(path, MetafileName), data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ArgumentException e)
|
catch (ArgumentException e)
|
||||||
@ -382,7 +387,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
|
public async Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
|
||||||
{
|
{
|
||||||
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
|
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
|
||||||
var imagePath = string.Empty;
|
var imagePath = string.Empty;
|
||||||
@ -427,9 +432,71 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
ImagePath = imagePath
|
ImagePath = imagePath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!await ReconcileManifest(manifest, path))
|
||||||
|
{
|
||||||
|
// An error occurred during reconciliation and saving could be undesirable.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return SaveManifest(manifest, path);
|
return SaveManifest(manifest, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
|
||||||
|
/// If no file is found, no reconciliation occurs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param>
|
||||||
|
/// <param name="path">The plugin path.</param>
|
||||||
|
/// <returns>The reconciled <see cref="PluginManifest"/>.</returns>
|
||||||
|
private async Task<bool> ReconcileManifest(PluginManifest manifest, string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var metafile = Path.Combine(path, MetafileName);
|
||||||
|
if (!File.Exists(metafile))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No local manifest exists for plugin {Plugin}. Skipping manifest reconciliation.", manifest.Name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var metaStream = File.OpenRead(metafile);
|
||||||
|
var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
|
||||||
|
localManifest ??= new PluginManifest();
|
||||||
|
|
||||||
|
if (!Equals(localManifest.Id, manifest.Id))
|
||||||
|
{
|
||||||
|
_logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", localManifest.Id, manifest.Id);
|
||||||
|
manifest.Status = PluginStatus.Malfunctioned;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localManifest.Version != manifest.Version)
|
||||||
|
{
|
||||||
|
// Package information provides the version and is the source of truth. Pre-packages meta.json is assumed to be a mistake in this regard.
|
||||||
|
_logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced.", localManifest.Version, manifest.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly mapping properties instead of using reflection is preferred here.
|
||||||
|
manifest.Category = string.IsNullOrEmpty(localManifest.Category) ? manifest.Category : localManifest.Category;
|
||||||
|
manifest.AutoUpdate = localManifest.AutoUpdate; // Preserve whatever is local. Package info does not have this property.
|
||||||
|
manifest.Changelog = string.IsNullOrEmpty(localManifest.Changelog) ? manifest.Changelog : localManifest.Changelog;
|
||||||
|
manifest.Description = string.IsNullOrEmpty(localManifest.Description) ? manifest.Description : localManifest.Description;
|
||||||
|
manifest.Name = string.IsNullOrEmpty(localManifest.Name) ? manifest.Name : localManifest.Name;
|
||||||
|
manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Overview;
|
||||||
|
manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner;
|
||||||
|
manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest.TargetAbi;
|
||||||
|
manifest.Timestamp = localManifest.Timestamp.Equals(default) ? manifest.Timestamp : localManifest.Timestamp;
|
||||||
|
manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest.ImagePath;
|
||||||
|
manifest.Assemblies = localManifest.Assemblies;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Unable to reconcile plugin manifest due to an error. {Path}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes a plugin's load status.
|
/// Changes a plugin's load status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -594,7 +661,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
{
|
{
|
||||||
Version? version;
|
Version? version;
|
||||||
PluginManifest? manifest = null;
|
PluginManifest? manifest = null;
|
||||||
var metafile = Path.Combine(dir, "meta.json");
|
var metafile = Path.Combine(dir, MetafileName);
|
||||||
if (File.Exists(metafile))
|
if (File.Exists(metafile))
|
||||||
{
|
{
|
||||||
// Only path where this stays null is when File.ReadAllBytes throws an IOException
|
// Only path where this stays null is when File.ReadAllBytes throws an IOException
|
||||||
@ -688,7 +755,15 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
var entry = versions[x];
|
var entry = versions[x];
|
||||||
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
|
if (!TryGetPluginDlls(entry, out var allowedDlls))
|
||||||
|
{
|
||||||
|
_logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name);
|
||||||
|
ChangePluginState(entry, PluginStatus.Malfunctioned);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.DllFiles = allowedDlls;
|
||||||
|
|
||||||
if (entry.IsEnabledAndSupported)
|
if (entry.IsEnabledAndSupported)
|
||||||
{
|
{
|
||||||
lastName = entry.Name;
|
lastName = entry.Name;
|
||||||
@ -734,6 +809,68 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
return versions.Where(p => p.DllFiles.Count != 0);
|
return versions.Where(p => p.DllFiles.Count != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist
|
||||||
|
/// from the manifest.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Loading DLLs from externally supplied paths introduces a path traversal risk. This method
|
||||||
|
/// uses a safelisting tactic of considering DLLs from the plugin directory and only using
|
||||||
|
/// the plugin's canonicalized assembly whitelist for comparison. See
|
||||||
|
/// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="plugin">The plugin.</param>
|
||||||
|
/// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will be empty.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory.
|
||||||
|
/// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception>
|
||||||
|
private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(nameof(plugin));
|
||||||
|
|
||||||
|
IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
whitelistedDlls = Array.Empty<string>();
|
||||||
|
if (pluginDlls.Count > 0 && plugin.Manifest.Assemblies.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Registering whitelisted assemblies for plugin \"{Plugin}\"...", plugin.Name);
|
||||||
|
|
||||||
|
var canonicalizedPaths = new List<string>();
|
||||||
|
foreach (var path in plugin.Manifest.Assemblies)
|
||||||
|
{
|
||||||
|
var canonicalized = Path.Combine(plugin.Path, path).Canonicalize();
|
||||||
|
|
||||||
|
// Ensure we stay in the plugin directory.
|
||||||
|
if (!canonicalized.StartsWith(plugin.Path.NormalizePath(), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogError("Assembly path {Path} is not inside the plugin directory.", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalizedPaths.Add(canonicalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var intersected = pluginDlls.Intersect(canonicalizedPaths).ToList();
|
||||||
|
|
||||||
|
if (intersected.Count != canonicalizedPaths.Count)
|
||||||
|
{
|
||||||
|
_logger.LogError("Plugin {Plugin} contained assembly paths that were not found in the directory.", plugin.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistedDlls = intersected;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No whitelist, default to loading all DLLs in plugin directory.
|
||||||
|
whitelistedDlls = pluginDlls;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes the status of the other versions of the plugin to "Superceded".
|
/// Changes the status of the other versions of the plugin to "Superceded".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
|
var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
|
||||||
if (plugin is not null)
|
if (plugin is not null)
|
||||||
{
|
{
|
||||||
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
|
await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove versions with a target ABI greater then the current application version.
|
// Remove versions with a target ABI greater then the current application version.
|
||||||
@ -555,7 +555,10 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
using var reader = new ZipArchive(stream);
|
using var reader = new ZipArchive(stream);
|
||||||
reader.ExtractToDirectory(targetDir, true);
|
reader.ExtractToDirectory(targetDir, true);
|
||||||
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
|
|
||||||
|
// Ensure we create one or populate existing ones with missing data.
|
||||||
|
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
|
||||||
|
|
||||||
_pluginManager.ImportPluginFrom(targetDir);
|
_pluginManager.ImportPluginFrom(targetDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Server.Migrations;
|
using Jellyfin.Server.Migrations;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.Net;
|
||||||
|
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||||
|
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||||
using MediaBrowser.Model.ApiClient;
|
using MediaBrowser.Model.ApiClient;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
using MediaBrowser.Model.SyncPlay;
|
using MediaBrowser.Model.SyncPlay;
|
||||||
using Microsoft.OpenApi.Any;
|
using Microsoft.OpenApi.Any;
|
||||||
@ -36,17 +40,141 @@ namespace Jellyfin.Server.Filters
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
{
|
{
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
|
context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
|
|
||||||
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
|
var webSocketTypes = typeof(WebSocketMessage).Assembly.GetTypes()
|
||||||
|
.Where(t => t.IsSubclassOf(typeof(WebSocketMessage))
|
||||||
|
&& !t.IsGenericType
|
||||||
|
&& t != typeof(WebSocketMessageInfo))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var inboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||||
|
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||||
|
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
|
||||||
|
{
|
||||||
|
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||||
|
if (messageType is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||||
|
inboundWebSocketSchemas.Add(schema);
|
||||||
|
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inboundWebSocketMessageSchema = new OpenApiSchema
|
||||||
|
{
|
||||||
|
Type = "object",
|
||||||
|
Description = "Represents the list of possible inbound websocket types",
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Id = nameof(InboundWebSocketMessage),
|
||||||
|
Type = ReferenceType.Schema
|
||||||
|
},
|
||||||
|
OneOf = inboundWebSocketSchemas,
|
||||||
|
Discriminator = new OpenApiDiscriminator
|
||||||
|
{
|
||||||
|
PropertyName = nameof(WebSocketMessage.MessageType),
|
||||||
|
Mapping = inboundWebSocketDiscriminators
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
|
||||||
|
|
||||||
|
var outboundWebSocketSchemas = new List<OpenApiSchema>();
|
||||||
|
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
|
||||||
|
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
|
||||||
|
{
|
||||||
|
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
|
||||||
|
if (messageType is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional discriminator needed for GroupUpdate models...
|
||||||
|
if (messageType == SessionMessageType.SyncPlayGroupUpdate && type != typeof(SyncPlayGroupUpdateCommandMessage))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||||
|
outboundWebSocketSchemas.Add(schema);
|
||||||
|
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
|
||||||
|
}
|
||||||
|
|
||||||
|
var outboundWebSocketMessageSchema = new OpenApiSchema
|
||||||
|
{
|
||||||
|
Type = "object",
|
||||||
|
Description = "Represents the list of possible outbound websocket types",
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Id = nameof(OutboundWebSocketMessage),
|
||||||
|
Type = ReferenceType.Schema
|
||||||
|
},
|
||||||
|
OneOf = outboundWebSocketSchemas,
|
||||||
|
Discriminator = new OpenApiDiscriminator
|
||||||
|
{
|
||||||
|
PropertyName = nameof(WebSocketMessage.MessageType),
|
||||||
|
Mapping = outboundWebSocketDiscriminators
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.SchemaRepository.AddDefinition(nameof(OutboundWebSocketMessage), outboundWebSocketMessageSchema);
|
||||||
|
context.SchemaRepository.AddDefinition(
|
||||||
|
nameof(WebSocketMessage),
|
||||||
|
new OpenApiSchema
|
||||||
|
{
|
||||||
|
Type = "object",
|
||||||
|
Description = "Represents the possible websocket types",
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Id = nameof(WebSocketMessage),
|
||||||
|
Type = ReferenceType.Schema
|
||||||
|
},
|
||||||
|
OneOf = new[]
|
||||||
|
{
|
||||||
|
inboundWebSocketMessageSchema,
|
||||||
|
outboundWebSocketMessageSchema
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually generate sync play GroupUpdate messages.
|
||||||
|
if (!context.SchemaRepository.Schemas.TryGetValue(nameof(GroupUpdate), out var groupUpdateSchema))
|
||||||
|
{
|
||||||
|
groupUpdateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupUpdateOfGroupInfoSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupInfoDto>), context.SchemaRepository);
|
||||||
|
var groupUpdateOfGroupStateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupStateUpdate>), context.SchemaRepository);
|
||||||
|
var groupUpdateOfStringSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<string>), context.SchemaRepository);
|
||||||
|
var groupUpdateOfPlayQueueSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<PlayQueueUpdate>), context.SchemaRepository);
|
||||||
|
|
||||||
|
groupUpdateSchema.OneOf = new List<OpenApiSchema>
|
||||||
|
{
|
||||||
|
groupUpdateOfGroupInfoSchema,
|
||||||
|
groupUpdateOfGroupStateSchema,
|
||||||
|
groupUpdateOfStringSchema,
|
||||||
|
groupUpdateOfPlayQueueSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
groupUpdateSchema.Discriminator = new OpenApiDiscriminator
|
||||||
|
{
|
||||||
|
PropertyName = nameof(GroupUpdate.Type),
|
||||||
|
Mapping = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ GroupUpdateType.UserJoined.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.UserLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.GroupJoined.ToString(), groupUpdateOfGroupInfoSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.GroupLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.StateUpdate.ToString(), groupUpdateOfGroupStateSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.PlayQueue.ToString(), groupUpdateOfPlayQueueSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.NotInGroup.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.GroupDoesNotExist.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
|
||||||
|
{ GroupUpdateType.LibraryAccessDenied.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
|
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
||||||
|
|
||||||
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
||||||
|
@ -57,7 +57,7 @@ namespace MediaBrowser.Common.Plugins
|
|||||||
/// <param name="path">The path where to save the manifest.</param>
|
/// <param name="path">The path where to save the manifest.</param>
|
||||||
/// <param name="status">Initial status of the plugin.</param>
|
/// <param name="status">Initial status of the plugin.</param>
|
||||||
/// <returns>True if successful.</returns>
|
/// <returns>True if successful.</returns>
|
||||||
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
|
Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Imports plugin details from a folder.
|
/// Imports plugin details from a folder.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ namespace MediaBrowser.Common.Plugins
|
|||||||
Overview = string.Empty;
|
Overview = string.Empty;
|
||||||
TargetAbi = string.Empty;
|
TargetAbi = string.Empty;
|
||||||
Version = string.Empty;
|
Version = string.Empty;
|
||||||
|
Assemblies = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -104,5 +106,12 @@ namespace MediaBrowser.Common.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("imagePath")]
|
[JsonPropertyName("imagePath")]
|
||||||
public string? ImagePath { get; set; }
|
public string? ImagePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of assemblies that should be loaded.
|
||||||
|
/// Paths are considered relative to the plugin folder.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("assemblies")]
|
||||||
|
public IReadOnlyList<string> Assemblies { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,16 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
public Series()
|
public Series()
|
||||||
{
|
{
|
||||||
AirDays = Array.Empty<DayOfWeek>();
|
AirDays = Array.Empty<DayOfWeek>();
|
||||||
|
SeasonNames = new Dictionary<int, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DayOfWeek[] AirDays { get; set; }
|
public DayOfWeek[] AirDays { get; set; }
|
||||||
|
|
||||||
public string AirTime { get; set; }
|
public string AirTime { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public Dictionary<int, string> SeasonNames { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override bool SupportsAddingToPlaylist => true;
|
public override bool SupportsAddingToPlaylist => true;
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||||
|
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 = new[]
|
private static readonly string[] _videoProfilesH264 = new[]
|
||||||
{
|
{
|
||||||
@ -162,7 +163,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
private bool IsVaapiFullSupported()
|
private bool IsVaapiFullSupported()
|
||||||
{
|
{
|
||||||
return _mediaEncoder.SupportsHwaccel("vaapi")
|
return _mediaEncoder.SupportsHwaccel("drm")
|
||||||
|
&& _mediaEncoder.SupportsHwaccel("vaapi")
|
||||||
&& _mediaEncoder.SupportsFilter("scale_vaapi")
|
&& _mediaEncoder.SupportsFilter("scale_vaapi")
|
||||||
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
|
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
|
||||||
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
|
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
|
||||||
@ -712,28 +714,43 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
options);
|
options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias)
|
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
|
||||||
{
|
{
|
||||||
alias ??= VaapiAlias;
|
alias ??= VaapiAlias;
|
||||||
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
|
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
|
||||||
var options = string.IsNullOrEmpty(driver)
|
var driverOpts = string.IsNullOrEmpty(driver)
|
||||||
? renderNodePath
|
? ":" + renderNodePath
|
||||||
: ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
|
: ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
|
||||||
|
var options = string.IsNullOrEmpty(srcDeviceAlias)
|
||||||
|
? driverOpts
|
||||||
|
: "@" + srcDeviceAlias;
|
||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" -init_hw_device vaapi={0}:{1}",
|
" -init_hw_device vaapi={0}{1}",
|
||||||
alias,
|
alias,
|
||||||
options);
|
options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetDrmDeviceArgs(string renderNodePath, string alias)
|
||||||
|
{
|
||||||
|
alias ??= DrmAlias;
|
||||||
|
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
|
||||||
|
|
||||||
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
" -init_hw_device drm={0}:{1}",
|
||||||
|
alias,
|
||||||
|
renderNodePath);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetQsvDeviceArgs(string alias)
|
private string GetQsvDeviceArgs(string alias)
|
||||||
{
|
{
|
||||||
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
|
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
{
|
{
|
||||||
// derive qsv from vaapi device
|
// derive qsv from vaapi device
|
||||||
return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias;
|
return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
@ -754,9 +771,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
|
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
|
||||||
{
|
{
|
||||||
|
// DVBSUB and DVDSUB use the fixed canvas size 720x576
|
||||||
if (state.SubtitleStream is not null
|
if (state.SubtitleStream is not null
|
||||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
|
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
|
||||||
&& !state.SubtitleStream.IsTextSubtitleStream)
|
&& !state.SubtitleStream.IsTextSubtitleStream
|
||||||
|
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var inW = state.VideoStream?.Width;
|
var inW = state.VideoStream?.Width;
|
||||||
var inH = state.VideoStream?.Height;
|
var inH = state.VideoStream?.Height;
|
||||||
@ -824,21 +844,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (_mediaEncoder.IsVaapiDeviceInteliHD)
|
if (_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||||
{
|
{
|
||||||
args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
|
||||||
}
|
}
|
||||||
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
||||||
{
|
{
|
||||||
// Only override i965 since it has lower priority than iHD in libva lookup.
|
// Only override i965 since it has lower priority than iHD in libva lookup.
|
||||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
|
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
|
||||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
|
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
|
||||||
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
|
var filterDevArgs = string.Empty;
|
||||||
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
|
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
|
||||||
|
|
||||||
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
|
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
|
||||||
@ -855,17 +871,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|
||||||
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||||
{
|
{
|
||||||
|
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
||||||
|
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
||||||
|
args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias));
|
||||||
|
|
||||||
// libplacebo wants an explicitly set vulkan filter device.
|
// libplacebo wants an explicitly set vulkan filter device.
|
||||||
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
|
|
||||||
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
|
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
|
||||||
}
|
}
|
||||||
else if (doOclTonemap)
|
else
|
||||||
|
{
|
||||||
|
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias));
|
||||||
|
filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
|
||||||
|
|
||||||
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
// ROCm/ROCr OpenCL runtime
|
// ROCm/ROCr OpenCL runtime
|
||||||
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
|
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
|
||||||
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
|
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (doOclTonemap)
|
else if (doOclTonemap)
|
||||||
{
|
{
|
||||||
args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
|
args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
|
||||||
@ -1549,11 +1574,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
param += " -preset p7";
|
param += " -preset p7";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "slow":
|
case "slower":
|
||||||
param += " -preset p6";
|
param += " -preset p6";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "slower":
|
case "slow":
|
||||||
param += " -preset p5";
|
param += " -preset p5";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -1586,8 +1611,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
switch (encodingOptions.EncoderPreset)
|
switch (encodingOptions.EncoderPreset)
|
||||||
{
|
{
|
||||||
case "veryslow":
|
case "veryslow":
|
||||||
case "slow":
|
|
||||||
case "slower":
|
case "slower":
|
||||||
|
case "slow":
|
||||||
param += " -quality quality";
|
param += " -quality quality";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -2929,7 +2954,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
|
public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(hwTonemapSuffix))
|
if (string.IsNullOrEmpty(hwTonemapSuffix))
|
||||||
{
|
{
|
||||||
@ -2941,7 +2966,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16";
|
args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32";
|
||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
args,
|
args,
|
||||||
@ -2972,14 +2998,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
|
args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
|
||||||
|
|
||||||
if (options.TonemappingParam != 0)
|
if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
args += ":param={5}";
|
if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode)
|
||||||
|
{
|
||||||
|
args += ":tonemap_mode={5}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
|
if (options.TonemappingParam != 0)
|
||||||
{
|
{
|
||||||
args += ":range={6}";
|
args += ":param={6}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
args += ":range={7}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2991,10 +3027,80 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
algorithm,
|
algorithm,
|
||||||
options.TonemappingPeak,
|
options.TonemappingPeak,
|
||||||
options.TonemappingDesat,
|
options.TonemappingDesat,
|
||||||
|
options.TonemappingMode,
|
||||||
options.TonemappingParam,
|
options.TonemappingParam,
|
||||||
options.TonemappingRange);
|
options.TonemappingRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetLibplaceboFilter(
|
||||||
|
EncodingOptions options,
|
||||||
|
string videoFormat,
|
||||||
|
bool doTonemap,
|
||||||
|
int? videoWidth,
|
||||||
|
int? videoHeight,
|
||||||
|
int? requestedWidth,
|
||||||
|
int? requestedHeight,
|
||||||
|
int? requestedMaxWidth,
|
||||||
|
int? requestedMaxHeight)
|
||||||
|
{
|
||||||
|
var (outWidth, outHeight) = GetFixedOutputSize(
|
||||||
|
videoWidth,
|
||||||
|
videoHeight,
|
||||||
|
requestedWidth,
|
||||||
|
requestedHeight,
|
||||||
|
requestedMaxWidth,
|
||||||
|
requestedMaxHeight);
|
||||||
|
|
||||||
|
var isFormatFixed = !string.IsNullOrEmpty(videoFormat);
|
||||||
|
var isSizeFixed = !videoWidth.HasValue
|
||||||
|
|| outWidth.Value != videoWidth.Value
|
||||||
|
|| !videoHeight.HasValue
|
||||||
|
|| outHeight.Value != videoHeight.Value;
|
||||||
|
|
||||||
|
var sizeArg = isSizeFixed ? (":w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty;
|
||||||
|
var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty;
|
||||||
|
var tonemapArg = string.Empty;
|
||||||
|
|
||||||
|
if (doTonemap)
|
||||||
|
{
|
||||||
|
var algorithm = options.TonemappingAlgorithm;
|
||||||
|
var mode = options.TonemappingMode;
|
||||||
|
var range = options.TonemappingRange;
|
||||||
|
|
||||||
|
if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
algorithm = "bt.2390";
|
||||||
|
}
|
||||||
|
else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
algorithm = "clip";
|
||||||
|
}
|
||||||
|
|
||||||
|
tonemapArg = ":tonemapping=" + algorithm;
|
||||||
|
|
||||||
|
if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tonemapArg += ":tonemapping_mode=" + mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
|
||||||
|
|
||||||
|
if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tonemapArg += ":range=" + range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"libplacebo=upscaler=none:downscaler=none{0}{1}{2}",
|
||||||
|
sizeArg,
|
||||||
|
formatArg,
|
||||||
|
tonemapArg);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the parameter of software filter chain.
|
/// Gets the parameter of software filter chain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -4224,7 +4330,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||||
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
|
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
|
||||||
var isSwEncoder = !isVaapiEncoder;
|
var isSwEncoder = !isVaapiEncoder;
|
||||||
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
|
|
||||||
|
|
||||||
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
||||||
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
||||||
@ -4253,26 +4358,31 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
mainFilters.Add(swDeintFilter);
|
mainFilters.Add(swDeintFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var outFormat = doVkTonemap ? "yuv420p10le" : "nv12";
|
if (doVkTonemap || hasSubs)
|
||||||
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
|
||||||
// sw scale
|
|
||||||
mainFilters.Add(swScaleFilter);
|
|
||||||
mainFilters.Add("format=" + outFormat);
|
|
||||||
|
|
||||||
// keep video at memory except vk tonemap,
|
|
||||||
// since the overhead caused by hwupload >>> using sw filter.
|
|
||||||
// sw => hw
|
|
||||||
if (doVkTonemap)
|
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload=derive_device=vaapi");
|
// sw => hw
|
||||||
mainFilters.Add("format=vaapi");
|
mainFilters.Add("hwupload=derive_device=vulkan");
|
||||||
mainFilters.Add("hwmap=derive_device=vulkan");
|
|
||||||
mainFilters.Add("format=vulkan");
|
mainFilters.Add("format=vulkan");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// sw scale
|
||||||
|
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
|
mainFilters.Add(swScaleFilter);
|
||||||
|
mainFilters.Add("format=nv12");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (isVaapiDecoder)
|
else if (isVaapiDecoder)
|
||||||
{
|
{
|
||||||
// INPUT vaapi surface(vram)
|
// INPUT vaapi surface(vram)
|
||||||
|
if (doVkTonemap || hasSubs)
|
||||||
|
{
|
||||||
|
// map from vaapi to vulkan/drm via interop (Vega/gfx9+).
|
||||||
|
mainFilters.Add("hwmap=derive_device=vulkan");
|
||||||
|
mainFilters.Add("format=vulkan");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// hw deint
|
// hw deint
|
||||||
if (doDeintH2645)
|
if (doDeintH2645)
|
||||||
{
|
{
|
||||||
@ -4280,72 +4390,49 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
mainFilters.Add(deintFilter);
|
mainFilters.Add(deintFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12");
|
|
||||||
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
|
||||||
|
|
||||||
// allocate extra pool sizes for overlay_vulkan
|
|
||||||
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs)
|
|
||||||
{
|
|
||||||
hwScaleFilter += ":extra_hw_frames=32";
|
|
||||||
}
|
|
||||||
|
|
||||||
// hw scale
|
// hw scale
|
||||||
|
var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
mainFilters.Add(hwScaleFilter);
|
mainFilters.Add(hwScaleFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
|
|
||||||
{
|
|
||||||
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
|
|
||||||
mainFilters.Add("hwmap=derive_device=vulkan");
|
|
||||||
mainFilters.Add("format=vulkan");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// vk tonemap
|
// vk libplacebo
|
||||||
if (doVkTonemap)
|
if (doVkTonemap || hasSubs)
|
||||||
{
|
{
|
||||||
var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12";
|
var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat);
|
mainFilters.Add(libplaceboFilter);
|
||||||
mainFilters.Add(tonemapFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doVkTonemap && isVaInVaOut && !hasSubs)
|
if (doVkTonemap && !hasSubs)
|
||||||
{
|
{
|
||||||
// OUTPUT vaapi(nv12/bgra) surface(vram)
|
// OUTPUT vaapi(nv12) surface(vram)
|
||||||
// reverse-mapping via vaapi-vulkan interop.
|
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
|
||||||
mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
|
mainFilters.Add("hwmap=derive_device=drm");
|
||||||
|
mainFilters.Add("format=drm_prime");
|
||||||
|
mainFilters.Add("hwmap=derive_device=vaapi");
|
||||||
mainFilters.Add("format=vaapi");
|
mainFilters.Add("format=vaapi");
|
||||||
|
|
||||||
|
// clear the surf->meta_offset and output nv12
|
||||||
|
mainFilters.Add("scale_vaapi=format=nv12");
|
||||||
|
|
||||||
|
// hw deint
|
||||||
|
if (doDeintH2645)
|
||||||
|
{
|
||||||
|
var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
|
||||||
|
mainFilters.Add(deintFilter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryOutput = false;
|
if (!hasSubs)
|
||||||
var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
|
|
||||||
if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
|
|
||||||
{
|
{
|
||||||
memoryOutput = true;
|
|
||||||
|
|
||||||
// OUTPUT nv12 surface(memory)
|
// OUTPUT nv12 surface(memory)
|
||||||
|
if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
|
||||||
|
{
|
||||||
mainFilters.Add("hwdownload");
|
mainFilters.Add("hwdownload");
|
||||||
mainFilters.Add("format=nv12");
|
mainFilters.Add("format=nv12");
|
||||||
}
|
}
|
||||||
|
|
||||||
// OUTPUT nv12 surface(memory)
|
if (isSwDecoder && isVaapiEncoder && !doVkTonemap)
|
||||||
if (isSwDecoder && isVaapiEncoder)
|
|
||||||
{
|
|
||||||
memoryOutput = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memoryOutput)
|
|
||||||
{
|
|
||||||
// text subtitles
|
|
||||||
if (hasTextSubs)
|
|
||||||
{
|
|
||||||
var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
|
|
||||||
mainFilters.Add(textSubtitlesFilter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memoryOutput && isVaapiEncoder)
|
|
||||||
{
|
|
||||||
if (!hasGraphicalSubs)
|
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload_vaapi");
|
mainFilters.Add("hwupload_vaapi");
|
||||||
}
|
}
|
||||||
@ -4354,8 +4441,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/* Make sub and overlay filters for subtitle stream */
|
/* Make sub and overlay filters for subtitle stream */
|
||||||
var subFilters = new List<string>();
|
var subFilters = new List<string>();
|
||||||
var overlayFilters = new List<string>();
|
var overlayFilters = new List<string>();
|
||||||
if (isVaInVaOut)
|
|
||||||
{
|
|
||||||
if (hasSubs)
|
if (hasSubs)
|
||||||
{
|
{
|
||||||
if (hasGraphicalSubs)
|
if (hasGraphicalSubs)
|
||||||
@ -4374,35 +4459,35 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
subFilters.Add(subTextSubtitlesFilter);
|
subFilters.Add(subTextSubtitlesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer vaapi hwupload to vulkan hwupload,
|
subFilters.Add("hwupload=derive_device=vulkan");
|
||||||
// Mesa RADV does not support a dedicated transfer queue.
|
|
||||||
subFilters.Add("hwupload=derive_device=vaapi");
|
|
||||||
subFilters.Add("format=vaapi");
|
|
||||||
subFilters.Add("hwmap=derive_device=vulkan");
|
|
||||||
subFilters.Add("format=vulkan");
|
subFilters.Add("format=vulkan");
|
||||||
|
|
||||||
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
|
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
|
||||||
|
|
||||||
// TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi.
|
if (isSwEncoder)
|
||||||
overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none");
|
{
|
||||||
|
// OUTPUT nv12 surface(memory)
|
||||||
// OUTPUT vaapi(nv12/bgra) surface(vram)
|
overlayFilters.Add("scale_vulkan=format=nv12");
|
||||||
// reverse-mapping via vaapi-vulkan interop.
|
overlayFilters.Add("hwdownload");
|
||||||
overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1");
|
overlayFilters.Add("format=nv12");
|
||||||
|
}
|
||||||
|
else if (isVaapiEncoder)
|
||||||
|
{
|
||||||
|
// OUTPUT vaapi(nv12) surface(vram)
|
||||||
|
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
|
||||||
|
overlayFilters.Add("hwmap=derive_device=drm");
|
||||||
|
overlayFilters.Add("format=drm_prime");
|
||||||
|
overlayFilters.Add("hwmap=derive_device=vaapi");
|
||||||
overlayFilters.Add("format=vaapi");
|
overlayFilters.Add("format=vaapi");
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (memoryOutput)
|
|
||||||
{
|
|
||||||
if (hasGraphicalSubs)
|
|
||||||
{
|
|
||||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
|
||||||
subFilters.Add(subSwScaleFilter);
|
|
||||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
|
||||||
|
|
||||||
if (isVaapiEncoder)
|
// clear the surf->meta_offset and output nv12
|
||||||
|
overlayFilters.Add("scale_vaapi=format=nv12");
|
||||||
|
|
||||||
|
// hw deint
|
||||||
|
if (doDeintH2645)
|
||||||
{
|
{
|
||||||
overlayFilters.Add("hwupload_vaapi");
|
var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
|
||||||
|
overlayFilters.Add(deintFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.
|
/// The sorted playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The sorted playlist, or play queue of the group.</value>
|
/// <value>The sorted playlist, or play queue of the group.</value>
|
||||||
private List<QueueItem> _sortedPlaylist = new List<QueueItem>();
|
private List<SyncPlayQueueItem> _sortedPlaylist = new List<SyncPlayQueueItem>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The shuffled playlist.
|
/// The shuffled playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The shuffled playlist, or play queue of the group.</value>
|
/// <value>The shuffled playlist, or play queue of the group.</value>
|
||||||
private List<QueueItem> _shuffledPlaylist = new List<QueueItem>();
|
private List<SyncPlayQueueItem> _shuffledPlaylist = new List<SyncPlayQueueItem>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
|
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
|
||||||
@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
/// Gets the current playlist considering the shuffle mode.
|
/// Gets the current playlist considering the shuffle mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The playlist.</returns>
|
/// <returns>The playlist.</returns>
|
||||||
public IReadOnlyList<QueueItem> GetPlaylist()
|
public IReadOnlyList<SyncPlayQueueItem> GetPlaylist()
|
||||||
{
|
{
|
||||||
return GetPlaylistInternal();
|
return GetPlaylistInternal();
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
_sortedPlaylist = CreateQueueItemsFromArray(items);
|
_sortedPlaylist = CreateQueueItemsFromArray(items);
|
||||||
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
||||||
{
|
{
|
||||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||||
_shuffledPlaylist.Shuffle();
|
_shuffledPlaylist.Shuffle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
{
|
{
|
||||||
if (PlayingItemIndex == NoPlayingItemIndex)
|
if (PlayingItemIndex == NoPlayingItemIndex)
|
||||||
{
|
{
|
||||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||||
_shuffledPlaylist.Shuffle();
|
_shuffledPlaylist.Shuffle();
|
||||||
}
|
}
|
||||||
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
|
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
|
||||||
{
|
{
|
||||||
// First time shuffle.
|
// First time shuffle.
|
||||||
var playingItem = _sortedPlaylist[PlayingItemIndex];
|
var playingItem = _sortedPlaylist[PlayingItemIndex];
|
||||||
_shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
|
_shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
|
||||||
_shuffledPlaylist.RemoveAt(PlayingItemIndex);
|
_shuffledPlaylist.RemoveAt(PlayingItemIndex);
|
||||||
_shuffledPlaylist.Shuffle();
|
_shuffledPlaylist.Shuffle();
|
||||||
_shuffledPlaylist.Insert(0, playingItem);
|
_shuffledPlaylist.Insert(0, playingItem);
|
||||||
@ -407,7 +407,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
/// Gets the next item in the playlist considering repeat mode and shuffle mode.
|
/// Gets the next item in the playlist considering repeat mode and shuffle mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The next item in the playlist.</returns>
|
/// <returns>The next item in the playlist.</returns>
|
||||||
public QueueItem GetNextItemPlaylistId()
|
public SyncPlayQueueItem GetNextItemPlaylistId()
|
||||||
{
|
{
|
||||||
int newIndex;
|
int newIndex;
|
||||||
var playlist = GetPlaylistInternal();
|
var playlist = GetPlaylistInternal();
|
||||||
@ -502,12 +502,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
/// Creates a list from the array of items. Each item is given an unique playlist identifier.
|
/// Creates a list from the array of items. Each item is given an unique playlist identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The list of queue items.</returns>
|
/// <returns>The list of queue items.</returns>
|
||||||
private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
|
private List<SyncPlayQueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
|
||||||
{
|
{
|
||||||
var list = new List<QueueItem>();
|
var list = new List<SyncPlayQueueItem>();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var queueItem = new QueueItem(item);
|
var queueItem = new SyncPlayQueueItem(item);
|
||||||
list.Add(queueItem);
|
list.Add(queueItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,7 +518,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
/// Gets the current playlist considering the shuffle mode.
|
/// Gets the current playlist considering the shuffle mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The playlist.</returns>
|
/// <returns>The playlist.</returns>
|
||||||
private List<QueueItem> GetPlaylistInternal()
|
private List<SyncPlayQueueItem> GetPlaylistInternal()
|
||||||
{
|
{
|
||||||
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
|
||||||
{
|
{
|
||||||
@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
|
|||||||
/// Gets the current playing item, depending on the shuffle mode.
|
/// Gets the current playing item, depending on the shuffle mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The playing item.</returns>
|
/// <returns>The playing item.</returns>
|
||||||
private QueueItem GetPlayingItem()
|
private SyncPlayQueueItem GetPlayingItem()
|
||||||
{
|
{
|
||||||
if (PlayingItemIndex == NoPlayingItemIndex)
|
if (PlayingItemIndex == NoPlayingItemIndex)
|
||||||
{
|
{
|
||||||
|
@ -27,13 +27,13 @@ public class EncodingOptions
|
|||||||
EnableTonemapping = false;
|
EnableTonemapping = false;
|
||||||
EnableVppTonemapping = false;
|
EnableVppTonemapping = false;
|
||||||
TonemappingAlgorithm = "bt2390";
|
TonemappingAlgorithm = "bt2390";
|
||||||
|
TonemappingMode = "auto";
|
||||||
TonemappingRange = "auto";
|
TonemappingRange = "auto";
|
||||||
TonemappingDesat = 0;
|
TonemappingDesat = 0;
|
||||||
TonemappingThreshold = 0.8;
|
|
||||||
TonemappingPeak = 100;
|
TonemappingPeak = 100;
|
||||||
TonemappingParam = 0;
|
TonemappingParam = 0;
|
||||||
VppTonemappingBrightness = 0;
|
VppTonemappingBrightness = 16;
|
||||||
VppTonemappingContrast = 1.2;
|
VppTonemappingContrast = 1;
|
||||||
H264Crf = 23;
|
H264Crf = 23;
|
||||||
H265Crf = 28;
|
H265Crf = 28;
|
||||||
DeinterlaceDoubleRate = false;
|
DeinterlaceDoubleRate = false;
|
||||||
@ -137,6 +137,11 @@ public class EncodingOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string TonemappingAlgorithm { get; set; }
|
public string TonemappingAlgorithm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the tone-mapping mode.
|
||||||
|
/// </summary>
|
||||||
|
public string TonemappingMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the tone-mapping range.
|
/// Gets or sets the tone-mapping range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -147,11 +152,6 @@ public class EncodingOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double TonemappingDesat { get; set; }
|
public double TonemappingDesat { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tone-mapping threshold.
|
|
||||||
/// </summary>
|
|
||||||
public double TonemappingThreshold { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the tone-mapping peak.
|
/// Gets or sets the tone-mapping peak.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -757,8 +757,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
if (options.AllowVideoStreamCopy)
|
if (options.AllowVideoStreamCopy)
|
||||||
{
|
{
|
||||||
// prefer direct copy profile
|
// prefer direct copy profile
|
||||||
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
|
||||||
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
|
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
|
||||||
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
||||||
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
||||||
|
|
||||||
@ -768,7 +768,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
|
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
|
||||||
{
|
{
|
||||||
var videoCodec = transcodingProfile.VideoCodec;
|
var videoCodec = videoStream?.Codec;
|
||||||
var container = transcodingProfile.Container;
|
var container = transcodingProfile.Container;
|
||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
@ -905,7 +905,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoCodec, container) &&
|
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||||
var isFirstAppliedCodecProfile = true;
|
var isFirstAppliedCodecProfile = true;
|
||||||
foreach (var i in appliedVideoConditions)
|
foreach (var i in appliedVideoConditions)
|
||||||
@ -937,7 +937,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioCodec, container) &&
|
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
||||||
isFirstAppliedCodecProfile = true;
|
isFirstAppliedCodecProfile = true;
|
||||||
foreach (var codecProfile in appliedAudioConditions)
|
foreach (var codecProfile in appliedAudioConditions)
|
||||||
@ -1176,7 +1176,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
profile,
|
profile,
|
||||||
"VideoCodecProfile",
|
"VideoCodecProfile",
|
||||||
profile.CodecProfiles
|
profile.CodecProfiles
|
||||||
.Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
|
.Where(codecProfile => codecProfile.Type == CodecType.Video &&
|
||||||
|
codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||||
!checkVideoConditions(codecProfile.ApplyConditions).Any())
|
!checkVideoConditions(codecProfile.ApplyConditions).Any())
|
||||||
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
|
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
|
||||||
|
|
||||||
@ -1585,7 +1586,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
bool? isSecondaryAudio)
|
bool? isSecondaryAudio)
|
||||||
{
|
{
|
||||||
return codecProfiles
|
return codecProfiles
|
||||||
.Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) &&
|
.Where(profile => profile.Type == CodecType.VideoAudio &&
|
||||||
|
profile.ContainsAnyCodec(codec, container) &&
|
||||||
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)))
|
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)))
|
||||||
.SelectMany(profile => profile.Conditions)
|
.SelectMany(profile => profile.Conditions)
|
||||||
.Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
|
.Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
|
||||||
@ -1602,7 +1604,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
bool checkConditions)
|
bool checkConditions)
|
||||||
{
|
{
|
||||||
var conditions = codecProfiles
|
var conditions = codecProfiles
|
||||||
.Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) &&
|
.Where(profile => profile.Type == CodecType.Audio &&
|
||||||
|
profile.ContainsAnyCodec(codec, container) &&
|
||||||
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)))
|
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)))
|
||||||
.SelectMany(profile => profile.Conditions);
|
.SelectMany(profile => profile.Conditions);
|
||||||
|
|
||||||
|
@ -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,24 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.SyncPlay
|
namespace MediaBrowser.Model.SyncPlay;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class GroupUpdate.
|
/// Group update without data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the data of the message.</typeparam>
|
public abstract class GroupUpdate
|
||||||
public class GroupUpdate<T>
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GroupUpdate{T}"/> class.
|
/// Initializes a new instance of the <see cref="GroupUpdate"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="groupId">The group identifier.</param>
|
/// <param name="groupId">The group identifier.</param>
|
||||||
/// <param name="type">The update type.</param>
|
protected GroupUpdate(Guid groupId)
|
||||||
/// <param name="data">The update data.</param>
|
|
||||||
public GroupUpdate(Guid groupId, GroupUpdateType type, T data)
|
|
||||||
{
|
{
|
||||||
GroupId = groupId;
|
GroupId = groupId;
|
||||||
Type = type;
|
|
||||||
Data = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -31,12 +26,5 @@ namespace MediaBrowser.Model.SyncPlay
|
|||||||
/// Gets the update type.
|
/// Gets the update type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The update type.</value>
|
/// <value>The update type.</value>
|
||||||
public GroupUpdateType Type { get; }
|
public GroupUpdateType Type { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the update data.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The update data.</value>
|
|
||||||
public T Data { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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="isPlaying">The playing item status.</param>
|
||||||
/// <param name="shuffleMode">The shuffle mode.</param>
|
/// <param name="shuffleMode">The shuffle mode.</param>
|
||||||
/// <param name="repeatMode">The repeat mode.</param>
|
/// <param name="repeatMode">The repeat mode.</param>
|
||||||
public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
|
public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<SyncPlayQueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
|
||||||
{
|
{
|
||||||
Reason = reason;
|
Reason = reason;
|
||||||
LastUpdate = lastUpdate;
|
LastUpdate = lastUpdate;
|
||||||
@ -47,7 +47,7 @@ namespace MediaBrowser.Model.SyncPlay
|
|||||||
/// Gets the playlist.
|
/// Gets the playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The playlist.</value>
|
/// <value>The playlist.</value>
|
||||||
public IReadOnlyList<QueueItem> Playlist { get; }
|
public IReadOnlyList<SyncPlayQueueItem> Playlist { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the playing item index in the playlist.
|
/// Gets the playing item index in the playlist.
|
||||||
|
@ -5,13 +5,13 @@ namespace MediaBrowser.Model.SyncPlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class QueueItem.
|
/// Class QueueItem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class QueueItem
|
public class SyncPlayQueueItem
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="QueueItem"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayQueueItem"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemId">The item identifier.</param>
|
/// <param name="itemId">The item identifier.</param>
|
||||||
public QueueItem(Guid itemId)
|
public SyncPlayQueueItem(Guid itemId)
|
||||||
{
|
{
|
||||||
ItemId = itemId;
|
ItemId = itemId;
|
||||||
}
|
}
|
@ -12,6 +12,7 @@ using Jellyfin.Extensions;
|
|||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
|
@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
RemoveObsoleteEpisodes(item);
|
RemoveObsoleteEpisodes(item);
|
||||||
RemoveObsoleteSeasons(item);
|
RemoveObsoleteSeasons(item);
|
||||||
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -67,6 +67,20 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
var sourceItem = source.Item;
|
var sourceItem = source.Item;
|
||||||
var targetItem = target.Item;
|
var targetItem = target.Item;
|
||||||
|
var sourceSeasonNames = sourceItem.SeasonNames;
|
||||||
|
var targetSeasonNames = targetItem.SeasonNames;
|
||||||
|
|
||||||
|
if (replaceData || targetSeasonNames.Count == 0)
|
||||||
|
{
|
||||||
|
targetItem.SeasonNames = sourceSeasonNames;
|
||||||
|
}
|
||||||
|
else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey))
|
||||||
|
{
|
||||||
|
foreach (var (number, name) in sourceSeasonNames)
|
||||||
|
{
|
||||||
|
targetSeasonNames.TryAdd(number, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
|
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
|
||||||
{
|
{
|
||||||
@ -86,7 +100,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteSeasons(Series series)
|
private void RemoveObsoleteSeasons(Series series)
|
||||||
{
|
{
|
||||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync.
|
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
|
||||||
var physicalSeasonNumbers = new HashSet<int>();
|
var physicalSeasonNumbers = new HashSet<int>();
|
||||||
var virtualSeasons = new List<Season>();
|
var virtualSeasons = new List<Season>();
|
||||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||||
@ -177,36 +191,43 @@ namespace MediaBrowser.Providers.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates seasons for all episodes that aren't in a season folder.
|
/// Creates seasons for all episodes if they don't exist.
|
||||||
/// If no season number can be determined, a dummy season will be created.
|
/// If no season number can be determined, a dummy season will be created.
|
||||||
|
/// Updates seasons names.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="series">The series.</param>
|
/// <param name="series">The series.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The async task.</returns>
|
/// <returns>The async task.</returns>
|
||||||
private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
|
private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var seasonNames = series.SeasonNames;
|
||||||
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
|
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
|
||||||
var episodesInSeriesFolder = seriesChildren
|
var seasons = seriesChildren.OfType<Season>().ToList();
|
||||||
|
var uniqueSeasonNumbers = seriesChildren
|
||||||
.OfType<Episode>()
|
.OfType<Episode>()
|
||||||
.Where(i => !i.IsInSeasonFolder);
|
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
|
||||||
|
.Distinct();
|
||||||
List<Season> seasons = seriesChildren.OfType<Season>().ToList();
|
|
||||||
|
|
||||||
// Loop through the unique season numbers
|
// Loop through the unique season numbers
|
||||||
foreach (var episode in episodesInSeriesFolder)
|
foreach (var seasonNumber in uniqueSeasonNumbers)
|
||||||
{
|
{
|
||||||
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
||||||
var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
|
|
||||||
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
||||||
|
string? seasonName = null;
|
||||||
|
|
||||||
|
if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp))
|
||||||
|
{
|
||||||
|
seasonName = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingSeason is null)
|
if (existingSeason is null)
|
||||||
{
|
{
|
||||||
var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false);
|
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||||
seasons.Add(season);
|
series.AddChild(season);
|
||||||
}
|
}
|
||||||
else if (existingSeason.IsVirtualItem)
|
else
|
||||||
{
|
{
|
||||||
existingSeason.IsVirtualItem = false;
|
existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
|
||||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,21 +237,17 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
|
/// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="series">The series.</param>
|
/// <param name="series">The series.</param>
|
||||||
|
/// <param name="seasonName">The season name.</param>
|
||||||
/// <param name="seasonNumber">The season number.</param>
|
/// <param name="seasonNumber">The season number.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The newly created season.</returns>
|
/// <returns>The newly created season.</returns>
|
||||||
private async Task<Season> CreateSeasonAsync(
|
private async Task<Season> CreateSeasonAsync(
|
||||||
Series series,
|
Series series,
|
||||||
|
string? seasonName,
|
||||||
int? seasonNumber,
|
int? seasonNumber,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
string seasonName = seasonNumber switch
|
seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
|
||||||
{
|
|
||||||
null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
|
|
||||||
0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
|
|
||||||
_ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
|
|
||||||
};
|
|
||||||
|
|
||||||
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
|
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
|
||||||
|
|
||||||
var season = new Season
|
var season = new Season
|
||||||
@ -251,5 +268,20 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
return season;
|
return season;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(seasonName))
|
||||||
|
{
|
||||||
|
seasonName = seasonNumber switch
|
||||||
|
{
|
||||||
|
null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
|
||||||
|
0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
|
||||||
|
_ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return seasonName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "seasonname":
|
||||||
|
{
|
||||||
|
var name = reader.ReadElementContentAsString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
item.Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
@ -110,6 +112,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "namedseason":
|
||||||
|
{
|
||||||
|
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
|
||||||
|
var name = reader.ReadElementContentAsString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(name) && parsed)
|
||||||
|
{
|
||||||
|
item.SeasonNames[seasonNumber] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
base.FetchDataFromXmlNode(reader, itemResult);
|
base.FetchDataFromXmlNode(reader, itemResult);
|
||||||
break;
|
break;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using Emby.Server.Implementations.Library;
|
using Emby.Server.Implementations.Library;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -73,5 +74,47 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
|||||||
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
|
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, '/', null)]
|
||||||
|
[InlineData(null, '\\', null)]
|
||||||
|
[InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
|
||||||
|
[InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
|
||||||
|
[InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
|
||||||
|
[InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
|
||||||
|
[InlineData("", '/', "")]
|
||||||
|
public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedPath, path.NormalizePath(separator));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/home/jeff/myfile.mkv")]
|
||||||
|
[InlineData("C:\\Users\\Jeff\\myfile.mkv")]
|
||||||
|
[InlineData("\\home/jeff\\myfile.mkv")]
|
||||||
|
public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path)
|
||||||
|
{
|
||||||
|
var separator = Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), path.NormalizePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/home/jeff/myfile.mkv", '/')]
|
||||||
|
[InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')]
|
||||||
|
[InlineData("\\home/jeff\\myfile.mkv", '/')]
|
||||||
|
public void NormalizePath_OutVar_Correct(string path, char expectedSeparator)
|
||||||
|
{
|
||||||
|
var result = path.NormalizePath(out var separator);
|
||||||
|
|
||||||
|
Assert.Equal(expectedSeparator, separator);
|
||||||
|
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizePath_SpecifyInvalidSeparator_ThrowsException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => string.Empty.NormalizePath('a'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using Emby.Server.Implementations.Library;
|
||||||
using Emby.Server.Implementations.Plugins;
|
using Emby.Server.Implementations.Plugins;
|
||||||
|
using Jellyfin.Extensions.Json;
|
||||||
|
using Jellyfin.Extensions.Json.Converters;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Updates;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -11,6 +20,21 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
|||||||
{
|
{
|
||||||
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
|
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
|
||||||
|
|
||||||
|
private string _tempPath = string.Empty;
|
||||||
|
|
||||||
|
private string _pluginPath = string.Empty;
|
||||||
|
|
||||||
|
private JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
public PluginManagerTests()
|
||||||
|
{
|
||||||
|
(_tempPath, _pluginPath) = GetTestPaths("plugin-" + Path.GetRandomFileName());
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_pluginPath);
|
||||||
|
|
||||||
|
_options = GetTestSerializerOptions();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SaveManifest_RoundTrip_Success()
|
public void SaveManifest_RoundTrip_Success()
|
||||||
{
|
{
|
||||||
@ -20,12 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
|||||||
Version = "1.0"
|
Version = "1.0"
|
||||||
};
|
};
|
||||||
|
|
||||||
var tempPath = Path.Combine(_testPathRoot, "manifest-" + Path.GetRandomFileName());
|
Assert.True(pluginManager.SaveManifest(manifest, _pluginPath));
|
||||||
Directory.CreateDirectory(tempPath);
|
|
||||||
|
|
||||||
Assert.True(pluginManager.SaveManifest(manifest, tempPath));
|
var res = pluginManager.LoadManifest(_pluginPath);
|
||||||
|
|
||||||
var res = pluginManager.LoadManifest(tempPath);
|
|
||||||
|
|
||||||
Assert.Equal(manifest.Category, res.Manifest.Category);
|
Assert.Equal(manifest.Category, res.Manifest.Category);
|
||||||
Assert.Equal(manifest.Changelog, res.Manifest.Changelog);
|
Assert.Equal(manifest.Changelog, res.Manifest.Changelog);
|
||||||
@ -40,6 +61,278 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
|||||||
Assert.Equal(manifest.Status, res.Manifest.Status);
|
Assert.Equal(manifest.Status, res.Manifest.Status);
|
||||||
Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate);
|
Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate);
|
||||||
Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath);
|
Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath);
|
||||||
|
Assert.Equal(manifest.Assemblies, res.Manifest.Assemblies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests safe traversal within the plugin directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dllFile">The safe path to evaluate.</param>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("./some.dll")]
|
||||||
|
[InlineData("some.dll")]
|
||||||
|
[InlineData("sub/path/some.dll")]
|
||||||
|
public void Constructor_DiscoversSafePluginAssembly_Status_Active(string dllFile)
|
||||||
|
{
|
||||||
|
var manifest = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Safe Assembly",
|
||||||
|
Assemblies = new string[] { dllFile }
|
||||||
|
};
|
||||||
|
|
||||||
|
var filename = Path.GetFileName(dllFile)!;
|
||||||
|
var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(dllPath);
|
||||||
|
File.Create(Path.Combine(dllPath, filename));
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
|
||||||
|
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||||
|
|
||||||
|
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
|
||||||
|
|
||||||
|
var expectedFullPath = Path.Combine(_pluginPath, dllFile).Canonicalize();
|
||||||
|
|
||||||
|
Assert.NotNull(res);
|
||||||
|
Assert.NotEmpty(pluginManager.Plugins);
|
||||||
|
Assert.Equal(PluginStatus.Active, res!.Status);
|
||||||
|
Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]);
|
||||||
|
Assert.StartsWith(_pluginPath, expectedFullPath, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests unsafe attempts to traverse to higher directories.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Attempts to load directories outside of the plugin should be
|
||||||
|
/// constrained. Path traversal, shell expansion, and double encoding
|
||||||
|
/// can be used to load unintended files.
|
||||||
|
/// See <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="unsafePath">The unsafe path to evaluate.</param>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/some.dll")] // Root path.
|
||||||
|
[InlineData("../some.dll")] // Simple traversal.
|
||||||
|
[InlineData("C:\\some.dll")] // Windows root path.
|
||||||
|
[InlineData("test.txt")] // Not a DLL
|
||||||
|
[InlineData(".././.././../some.dll")] // Traversal with current and parent
|
||||||
|
[InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent
|
||||||
|
[InlineData("\\\\network\\resource.dll")] // UNC Path
|
||||||
|
[InlineData("https://jellyfin.org/some.dll")] // URL
|
||||||
|
[InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character.
|
||||||
|
public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath)
|
||||||
|
{
|
||||||
|
var manifest = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Unsafe Assembly",
|
||||||
|
Assemblies = new string[] { unsafePath }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only create very specific files. Otherwise the test will be exploiting path traversal.
|
||||||
|
var files = new string[]
|
||||||
|
{
|
||||||
|
"../other.dll",
|
||||||
|
"some.dll"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
File.Create(Path.Combine(_pluginPath, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
|
||||||
|
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||||
|
|
||||||
|
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
|
||||||
|
|
||||||
|
Assert.NotNull(res);
|
||||||
|
Assert.Empty(pluginManager.Plugins);
|
||||||
|
Assert.Equal(PluginStatus.Malfunctioned, res!.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PopulateManifest_ExistingMetafilePlugin_PopulatesMissingFields()
|
||||||
|
{
|
||||||
|
var packageInfo = GenerateTestPackage();
|
||||||
|
|
||||||
|
// Partial plugin without a name, but matching version and package ID
|
||||||
|
var partial = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = packageInfo.Id,
|
||||||
|
AutoUpdate = false, // Turn off AutoUpdate
|
||||||
|
Status = PluginStatus.Restart,
|
||||||
|
Version = new Version(1, 0, 0).ToString(),
|
||||||
|
Assemblies = new[] { "Jellyfin.Test.dll" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedManifest = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = partial.Id,
|
||||||
|
Name = packageInfo.Name,
|
||||||
|
AutoUpdate = partial.AutoUpdate,
|
||||||
|
Status = PluginStatus.Active,
|
||||||
|
Owner = packageInfo.Owner,
|
||||||
|
Assemblies = partial.Assemblies,
|
||||||
|
Category = packageInfo.Category,
|
||||||
|
Description = packageInfo.Description,
|
||||||
|
Overview = packageInfo.Overview,
|
||||||
|
TargetAbi = packageInfo.Versions[0].TargetAbi!,
|
||||||
|
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
|
||||||
|
Changelog = packageInfo.Versions[0].Changelog!,
|
||||||
|
Version = new Version(1, 0).ToString(),
|
||||||
|
ImagePath = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||||
|
|
||||||
|
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||||
|
|
||||||
|
var resultBytes = File.ReadAllBytes(metafilePath);
|
||||||
|
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equivalent(expectedManifest, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PopulateManifest_NoMetafile_PreservesManifest()
|
||||||
|
{
|
||||||
|
var packageInfo = GenerateTestPackage();
|
||||||
|
var expectedManifest = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = packageInfo.Id,
|
||||||
|
Name = packageInfo.Name,
|
||||||
|
AutoUpdate = true,
|
||||||
|
Status = PluginStatus.Active,
|
||||||
|
Owner = packageInfo.Owner,
|
||||||
|
Assemblies = Array.Empty<string>(),
|
||||||
|
Category = packageInfo.Category,
|
||||||
|
Description = packageInfo.Description,
|
||||||
|
Overview = packageInfo.Overview,
|
||||||
|
TargetAbi = packageInfo.Versions[0].TargetAbi!,
|
||||||
|
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
|
||||||
|
Changelog = packageInfo.Versions[0].Changelog!,
|
||||||
|
Version = packageInfo.Versions[0].Version,
|
||||||
|
ImagePath = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0));
|
||||||
|
|
||||||
|
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||||
|
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
var resultBytes = File.ReadAllBytes(metafilePath);
|
||||||
|
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equivalent(expectedManifest, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PopulateManifest_ExistingMetafileMismatchedIds_Status_Malfunctioned()
|
||||||
|
{
|
||||||
|
var packageInfo = GenerateTestPackage();
|
||||||
|
|
||||||
|
// Partial plugin without a name, but matching version and package ID
|
||||||
|
var partial = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Version = new Version(1, 0, 0).ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||||
|
|
||||||
|
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||||
|
|
||||||
|
var resultBytes = File.ReadAllBytes(metafilePath);
|
||||||
|
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(packageInfo.Name, result.Name);
|
||||||
|
Assert.Equal(PluginStatus.Malfunctioned, result.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PopulateManifest_ExistingMetafileMismatchedVersions_Updates_Version()
|
||||||
|
{
|
||||||
|
var packageInfo = GenerateTestPackage();
|
||||||
|
|
||||||
|
var partial = new PluginManifest
|
||||||
|
{
|
||||||
|
Id = packageInfo.Id,
|
||||||
|
Version = new Version(2, 0, 0).ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||||
|
File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||||
|
|
||||||
|
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||||
|
|
||||||
|
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||||
|
|
||||||
|
var resultBytes = File.ReadAllBytes(metafilePath);
|
||||||
|
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(packageInfo.Name, result.Name);
|
||||||
|
Assert.Equal(PluginStatus.Active, result.Status);
|
||||||
|
Assert.Equal(packageInfo.Versions[0].Version, result.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackageInfo GenerateTestPackage()
|
||||||
|
{
|
||||||
|
var fixture = new Fixture();
|
||||||
|
fixture.Customize<PackageInfo>(c => c.Without(x => x.Versions).Without(x => x.ImageUrl));
|
||||||
|
fixture.Customize<VersionInfo>(c => c.Without(x => x.Version).Without(x => x.Timestamp));
|
||||||
|
|
||||||
|
var versionInfo = fixture.Create<VersionInfo>();
|
||||||
|
versionInfo.Version = new Version(1, 0).ToString();
|
||||||
|
versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var packageInfo = fixture.Create<PackageInfo>();
|
||||||
|
packageInfo.Versions = new[] { versionInfo };
|
||||||
|
|
||||||
|
return packageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonSerializerOptions GetTestSerializerOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions(JsonDefaults.Options)
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < options.Converters.Count; i++)
|
||||||
|
{
|
||||||
|
// Remove the Guid converter for parity with plugin manager.
|
||||||
|
if (options.Converters[i] is JsonGuidConverter converter)
|
||||||
|
{
|
||||||
|
options.Converters.Remove(converter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName)
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(_testPathRoot, "plugin-manager" + Path.GetRandomFileName());
|
||||||
|
var pluginPath = Path.Combine(tempPath, pluginFolderName);
|
||||||
|
|
||||||
|
return (tempPath, pluginPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user