mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 16:18:06 +00:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b05f4e2b | ||
|
|
e8148ec0bd | ||
|
|
77c5c53598 | ||
|
|
d2db73b876 | ||
|
|
49873c3d7f | ||
|
|
0b577c8a44 | ||
|
|
6386606a53 | ||
|
|
cc908210d9 | ||
|
|
dec30ade8f | ||
|
|
55a8d2555e | ||
|
|
b7c3510da1 | ||
|
|
fd102abd81 | ||
|
|
f8f7767cc5 | ||
|
|
e8436814dc | ||
|
|
14f63e8f2f | ||
|
|
e764de0c80 | ||
|
|
40147c9bb7 | ||
|
|
3566d21ad1 | ||
|
|
4c8df4c5bb | ||
|
|
9798bf29f3 | ||
|
|
93ce087fc9 | ||
|
|
e39495354b | ||
|
|
ee94fad8f7 | ||
|
|
53239b0529 | ||
|
|
cf0da1de86 | ||
|
|
093510ae58 | ||
|
|
c3fafe9289 | ||
|
|
bd914acd16 | ||
|
|
81f9bec101 | ||
|
|
7db8601fbc | ||
|
|
1ec247f5d8 | ||
|
|
11e9173fbc | ||
|
|
34508286a8 | ||
|
|
a82eded845 | ||
|
|
fcb729ff6b | ||
|
|
69f30bc52c | ||
|
|
3b605b6280 | ||
|
|
e8a359f97b | ||
|
|
dbfaafc08a | ||
|
|
de6747f6c5 | ||
|
|
100fe40b0a | ||
|
|
2197d20783 | ||
|
|
f4f9ab777f | ||
|
|
9ca7d62709 | ||
|
|
7aad16b6ec | ||
|
|
f77673438e | ||
|
|
bc2eb9fa79 | ||
|
|
df69ce55f7 | ||
|
|
93cca4d50e | ||
|
|
3c64bcffe3 | ||
|
|
6ece01d425 | ||
|
|
53f333bd64 | ||
|
|
9e459090ed | ||
|
|
95a4fc0f18 | ||
|
|
62bf3db885 | ||
|
|
42d702c091 | ||
|
|
d07fe14814 | ||
|
|
970eaf8dfb | ||
|
|
bc27c2b7da | ||
|
|
37b969304a | ||
|
|
c6b5c4dda5 | ||
|
|
51f5da8015 | ||
|
|
6e89ca9a34 | ||
|
|
de1896828f | ||
|
|
7d1d159b8a | ||
|
|
c3c98331d9 | ||
|
|
e78fa8c3ef | ||
|
|
d63fb437c6 | ||
|
|
25c6388e23 | ||
|
|
8f16e10fc6 | ||
|
|
1f07586d1c | ||
|
|
5e0f480e48 | ||
|
|
210d10400a |
@@ -1,59 +0,0 @@
|
||||
parameters:
|
||||
- name: LinuxImage
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
- name: GeneratorVersion
|
||||
type: string
|
||||
default: "5.0.1"
|
||||
|
||||
jobs:
|
||||
- job: GenerateApiClients
|
||||
displayName: 'Generate Api Clients'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
dependsOn: Test
|
||||
|
||||
pool:
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download OpenAPI Spec Artifact'
|
||||
inputs:
|
||||
source: 'current'
|
||||
artifact: "OpenAPI Spec"
|
||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
||||
runVersion: "latest"
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Download OpenApi Generator'
|
||||
inputs:
|
||||
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
|
||||
|
||||
## Authenticate with npm registry
|
||||
- task: npmAuthenticate@0
|
||||
inputs:
|
||||
workingFile: ./.npmrc
|
||||
customEndpoint: 'jellyfin-bot for NPM'
|
||||
|
||||
## Generate npm api client
|
||||
- task: CmdLine@2
|
||||
displayName: 'Build stable typescript axios client'
|
||||
inputs:
|
||||
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
|
||||
|
||||
## Run npm install
|
||||
- task: Npm@1
|
||||
displayName: 'Install npm dependencies'
|
||||
inputs:
|
||||
command: install
|
||||
workingDir: ./apiclient/generated/typescript/axios
|
||||
|
||||
## Publish npm packages
|
||||
- task: Npm@1
|
||||
displayName: 'Publish stable typescript axios client'
|
||||
inputs:
|
||||
command: custom
|
||||
customCommand: publish --access public
|
||||
publishRegistry: useExternalRegistry
|
||||
publishEndpoint: 'jellyfin-bot for NPM'
|
||||
workingDir: ./apiclient/generated/typescript/axios
|
||||
@@ -61,6 +61,3 @@ jobs:
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-package.yml
|
||||
|
||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
||||
- template: azure-pipelines-api-client.yml
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
- [shemanaev](https://github.com/shemanaev)
|
||||
- [skaro13](https://github.com/skaro13)
|
||||
- [sl1288](https://github.com/sl1288)
|
||||
- [Smith00101010](https://github.com/Smith00101010)
|
||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [spookbits](https://github.com/spookbits)
|
||||
@@ -143,6 +144,7 @@
|
||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||
- [skyfrk](https://github.com/skyfrk)
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Emby.Dlna
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -138,80 +138,45 @@ namespace Emby.Dlna
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||
/// - the profile field can be an exact match, or a reg exp.
|
||||
/// </summary>
|
||||
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||
/// <returns><b>True</b> if they match.</returns>
|
||||
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
||||
{
|
||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
||||
{
|
||||
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
||||
{
|
||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
||||
{
|
||||
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
||||
{
|
||||
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
||||
{
|
||||
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
||||
{
|
||||
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
||||
{
|
||||
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||
}
|
||||
|
||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
// In profile identification: An empty pattern matches anything.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
// The profile contains a value, and the device doesn't.
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||
if (command == null)
|
||||
{
|
||||
return false;
|
||||
@@ -253,7 +253,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -278,7 +278,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -305,7 +305,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -378,6 +378,10 @@ namespace Emby.Dlna.PlayTo
|
||||
public async Task SetPlay(CancellationToken cancellationToken)
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (avCommands == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -388,7 +392,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
||||
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -406,7 +410,7 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
||||
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -528,7 +532,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
||||
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -578,7 +582,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
||||
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
||||
if (command == null)
|
||||
{
|
||||
return;
|
||||
@@ -665,6 +669,10 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (rendererCommands == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
@@ -733,6 +741,11 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (rendererCommands == null)
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
@@ -914,6 +927,10 @@ namespace Emby.Dlna.PlayTo
|
||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||
|
||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AvCommands = TransportCommands.Create(document);
|
||||
return AvCommands;
|
||||
@@ -942,6 +959,10 @@ namespace Emby.Dlna.PlayTo
|
||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RendererCommands = TransportCommands.Create(document);
|
||||
return RendererCommands;
|
||||
@@ -973,6 +994,10 @@ namespace Emby.Dlna.PlayTo
|
||||
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
||||
|
||||
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
if (document == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var friendlyNames = new List<string>();
|
||||
|
||||
|
||||
@@ -943,11 +943,7 @@ namespace Emby.Dlna.PlayTo
|
||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||
|
||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
||||
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||
|
||||
@@ -178,12 +178,17 @@ namespace Emby.Dlna.PlayTo
|
||||
if (controller == null)
|
||||
{
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (device == null)
|
||||
{
|
||||
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string deviceName = device.Properties.Name;
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
|
||||
@@ -45,10 +45,10 @@ namespace Emby.Dlna.PlayTo
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.PreserveWhitespace,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
@@ -94,10 +94,17 @@ namespace Emby.Dlna.PlayTo
|
||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return XDocument.Parse(
|
||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
||||
LoadOptions.PreserveWhitespace);
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.PreserveWhitespace,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||
|
||||
@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
|
||||
public class TransportCommands
|
||||
{
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
||||
|
||||
public List<StateVariable> StateVariables => _stateVariables;
|
||||
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||
|
||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
||||
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||
|
||||
public static TransportCommands Create(XDocument document)
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
LocalIpAddress = e.LocalIpAddress
|
||||
RemoteIpAddress = e.RemoteIpAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -171,11 +172,26 @@ namespace Emby.Drawing
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
}
|
||||
|
||||
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
|
||||
int quality = options.Quality;
|
||||
|
||||
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
||||
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
|
||||
string cacheFilePath = GetCacheFilePath(
|
||||
originalImagePath,
|
||||
options.Width,
|
||||
options.Height,
|
||||
options.MaxWidth,
|
||||
options.MaxHeight,
|
||||
options.FillWidth,
|
||||
options.FillHeight,
|
||||
quality,
|
||||
dateModified,
|
||||
outputFormat,
|
||||
options.AddPlayedIndicator,
|
||||
options.PercentPlayed,
|
||||
options.UnplayedCount,
|
||||
options.Blur,
|
||||
options.BackgroundColor,
|
||||
options.ForegroundLayer);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -246,48 +262,111 @@ namespace Emby.Drawing
|
||||
/// <summary>
|
||||
/// Gets the cache file path based on a set of parameters.
|
||||
/// </summary>
|
||||
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
|
||||
private string GetCacheFilePath(
|
||||
string originalPath,
|
||||
int? width,
|
||||
int? height,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? fillWidth,
|
||||
int? fillHeight,
|
||||
int quality,
|
||||
DateTime dateModified,
|
||||
ImageFormat format,
|
||||
bool addPlayedIndicator,
|
||||
double percentPlayed,
|
||||
int? unwatchedCount,
|
||||
int? blur,
|
||||
string backgroundColor,
|
||||
string foregroundLayer)
|
||||
{
|
||||
var filename = originalPath
|
||||
+ "width=" + outputSize.Width
|
||||
+ "height=" + outputSize.Height
|
||||
+ "quality=" + quality
|
||||
+ "datemodified=" + dateModified.Ticks
|
||||
+ "f=" + format;
|
||||
var filename = new StringBuilder(256);
|
||||
filename.Append(originalPath);
|
||||
|
||||
filename.Append(",quality=");
|
||||
filename.Append(quality);
|
||||
|
||||
filename.Append(",datemodified=");
|
||||
filename.Append(dateModified.Ticks);
|
||||
|
||||
filename.Append(",f=");
|
||||
filename.Append(format);
|
||||
|
||||
if (width.HasValue)
|
||||
{
|
||||
filename.Append(",width=");
|
||||
filename.Append(width.Value);
|
||||
}
|
||||
|
||||
if (height.HasValue)
|
||||
{
|
||||
filename.Append(",height=");
|
||||
filename.Append(height.Value);
|
||||
}
|
||||
|
||||
if (maxWidth.HasValue)
|
||||
{
|
||||
filename.Append(",maxwidth=");
|
||||
filename.Append(maxWidth.Value);
|
||||
}
|
||||
|
||||
if (maxHeight.HasValue)
|
||||
{
|
||||
filename.Append(",maxheight=");
|
||||
filename.Append(maxHeight.Value);
|
||||
}
|
||||
|
||||
if (fillWidth.HasValue)
|
||||
{
|
||||
filename.Append(",fillwidth=");
|
||||
filename.Append(fillWidth.Value);
|
||||
}
|
||||
|
||||
if (fillHeight.HasValue)
|
||||
{
|
||||
filename.Append(",fillheight=");
|
||||
filename.Append(fillHeight.Value);
|
||||
}
|
||||
|
||||
if (addPlayedIndicator)
|
||||
{
|
||||
filename += "pl=true";
|
||||
filename.Append(",pl=true");
|
||||
}
|
||||
|
||||
if (percentPlayed > 0)
|
||||
{
|
||||
filename += "p=" + percentPlayed;
|
||||
filename.Append(",p=");
|
||||
filename.Append(percentPlayed);
|
||||
}
|
||||
|
||||
if (unwatchedCount.HasValue)
|
||||
{
|
||||
filename += "p=" + unwatchedCount.Value;
|
||||
filename.Append(",p=");
|
||||
filename.Append(unwatchedCount.Value);
|
||||
}
|
||||
|
||||
if (blur.HasValue)
|
||||
{
|
||||
filename += "blur=" + blur.Value;
|
||||
filename.Append(",blur=");
|
||||
filename.Append(blur.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(backgroundColor))
|
||||
{
|
||||
filename += "b=" + backgroundColor;
|
||||
filename.Append(",b=");
|
||||
filename.Append(backgroundColor);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(foregroundLayer))
|
||||
{
|
||||
filename += "fl=" + foregroundLayer;
|
||||
filename.Append(",fl=");
|
||||
filename.Append(foregroundLayer);
|
||||
}
|
||||
|
||||
filename += "v=" + Version;
|
||||
filename.Append(",v=");
|
||||
filename.Append(Version);
|
||||
|
||||
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
|
||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.7.1</VersionPrefix>
|
||||
<VersionPrefix>10.7.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
private IEnumerable<BoxSet> GetCollections(User user)
|
||||
{
|
||||
var folder = GetCollectionsFolder(false).Result;
|
||||
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||
|
||||
return folder == null
|
||||
? Enumerable.Empty<BoxSet>()
|
||||
@@ -319,11 +319,11 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var results = new Dictionary<Guid, BaseItem>();
|
||||
|
||||
var allBoxsets = GetCollections(user).ToList();
|
||||
var allBoxSets = GetCollections(user).ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!(item is ISupportsBoxSetGrouping))
|
||||
if (item is not ISupportsBoxSetGrouping)
|
||||
{
|
||||
results[item.Id] = item;
|
||||
}
|
||||
@@ -331,33 +331,44 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var itemId = item.Id;
|
||||
|
||||
var currentBoxSets = allBoxsets
|
||||
.Where(i => i.ContainsLinkedChildByItemId(itemId))
|
||||
.ToList();
|
||||
|
||||
if (currentBoxSets.Count > 0)
|
||||
var itemIsInBoxSet = false;
|
||||
foreach (var boxSet in allBoxSets)
|
||||
{
|
||||
foreach (var boxset in currentBoxSets)
|
||||
if (!boxSet.ContainsLinkedChildByItemId(itemId))
|
||||
{
|
||||
results[boxset.Id] = boxset;
|
||||
continue;
|
||||
}
|
||||
|
||||
itemIsInBoxSet = true;
|
||||
|
||||
results.TryAdd(boxSet.Id, boxSet);
|
||||
}
|
||||
|
||||
// skip any item that is in a box set
|
||||
if (itemIsInBoxSet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var alreadyInResults = false;
|
||||
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||
if (item is Video video)
|
||||
{
|
||||
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||
{
|
||||
if (!results.ContainsKey(childId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
alreadyInResults = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var alreadyInResults = false;
|
||||
foreach (var child in item.GetMediaSources(true))
|
||||
{
|
||||
if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
|
||||
{
|
||||
alreadyInResults = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyInResults)
|
||||
{
|
||||
results[item.Id] = item;
|
||||
}
|
||||
if (!alreadyInResults)
|
||||
{
|
||||
results[itemId] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5415,7 +5415,6 @@ AND Type = @InternalPersonType)");
|
||||
ItemIds = query.ItemIds,
|
||||
TopParentIds = query.TopParentIds,
|
||||
ParentId = query.ParentId,
|
||||
IsPlayed = query.IsPlayed,
|
||||
IsAiring = query.IsAiring,
|
||||
IsMovie = query.IsMovie,
|
||||
IsSports = query.IsSports,
|
||||
@@ -5441,6 +5440,7 @@ AND Type = @InternalPersonType)");
|
||||
|
||||
var outerQuery = new InternalItemsQuery(query.User)
|
||||
{
|
||||
IsPlayed = query.IsPlayed,
|
||||
IsFavorite = query.IsFavorite,
|
||||
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
|
||||
IsLiked = query.IsLiked,
|
||||
|
||||
@@ -106,8 +106,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
NatUtility.StartDiscovery();
|
||||
|
||||
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||
|
||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
@@ -118,13 +116,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
||||
|
||||
_timer?.Dispose();
|
||||
|
||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
|
||||
}
|
||||
|
||||
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||
|
||||
@@ -14,15 +14,18 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
public class WebSocketManager : IWebSocketManager
|
||||
{
|
||||
private readonly IWebSocketListener[] _webSocketListeners;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILogger<WebSocketManager> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public WebSocketManager(
|
||||
IAuthService authService,
|
||||
IEnumerable<IWebSocketListener> webSocketListeners,
|
||||
ILogger<WebSocketManager> logger,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_webSocketListeners = webSocketListeners.ToArray();
|
||||
_authService = authService;
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
@@ -30,6 +33,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
public async Task WebSocketRequestHandler(HttpContext context)
|
||||
{
|
||||
_ = _authService.Authenticate(context.Request);
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||
|
||||
@@ -249,9 +249,18 @@ namespace Emby.Server.Implementations.IO
|
||||
// Issue #2354 get the size of files behind symbolic links
|
||||
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
{
|
||||
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
|
||||
try
|
||||
{
|
||||
result.Length = thisFileStream.Length;
|
||||
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
|
||||
{
|
||||
result.Length = thisFileStream.Length;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
||||
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1914,12 +1914,17 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
_logger.LogWarning("Cannot get image index for {0}", img.Path);
|
||||
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||
continue;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
|
||||
{
|
||||
_logger.LogWarning("Cannot fetch image from {0}", img.Path);
|
||||
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||
continue;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1932,7 +1937,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
continue;
|
||||
@@ -1944,7 +1949,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
|
||||
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
|
||||
image.BlurHash = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1954,7 +1959,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
|
||||
_logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2875,6 +2880,12 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
|
||||
{
|
||||
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!item.SupportsPeople)
|
||||
{
|
||||
@@ -2882,6 +2893,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
_itemRepository.UpdatePeople(item.Id, people);
|
||||
|
||||
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
|
||||
@@ -2985,6 +2998,58 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
|
||||
{
|
||||
var personsToSave = new List<BaseItem>();
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||
var saveEntity = false;
|
||||
var personEntity = GetPerson(person.Name);
|
||||
|
||||
// if PresentationUniqueKey is empty it's likely a new item.
|
||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
||||
{
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
}
|
||||
|
||||
foreach (var id in person.ProviderIds)
|
||||
{
|
||||
if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
personEntity.SetProviderId(id.Key, id.Value);
|
||||
saveEntity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
|
||||
{
|
||||
personEntity.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = person.ImageUrl,
|
||||
Type = ImageType.Primary
|
||||
},
|
||||
0);
|
||||
|
||||
saveEntity = true;
|
||||
itemUpdateType = ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
if (saveEntity)
|
||||
{
|
||||
personsToSave.Add(personEntity);
|
||||
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CreateItems(personsToSave, null, CancellationToken.None);
|
||||
}
|
||||
|
||||
private void StartScanInBackground()
|
||||
{
|
||||
Task.Run(() =>
|
||||
|
||||
@@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
|
||||
return SortMediaSources(list);
|
||||
}
|
||||
|
||||
public MediaProtocol GetPathProtocol(string path)
|
||||
@@ -437,7 +437,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||
{
|
||||
return sources.OrderBy(i =>
|
||||
{
|
||||
@@ -452,8 +452,9 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var stream = i.VideoStream;
|
||||
|
||||
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
|
||||
return stream?.Width ?? 0;
|
||||
})
|
||||
.Where(i => i.Type != MediaSourceType.Placeholder)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,14 @@ namespace Emby.Server.Implementations.Library
|
||||
// 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
|
||||
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
|
||||
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
|
||||
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path.Length > subPath.Length
|
||||
&& !oldSubPathEndsWithSeparator
|
||||
&& path[subPath.Length] != newDirectorySeparatorChar)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resolvedItem.Files.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var firstMedia = resolvedItem.Files[0];
|
||||
|
||||
var libraryItem = new T
|
||||
|
||||
@@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileNameWithoutExtension(args.Path),
|
||||
IsInMixedFolder = true
|
||||
IsInMixedFolder = true,
|
||||
PlaylistMediaType = MediaType.Audio
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,15 +248,15 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
|
||||
{
|
||||
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
||||
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
||||
var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
||||
var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
||||
|
||||
if (minIn > _config.Configuration.MinAudiobookResume)
|
||||
if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
|
||||
{
|
||||
// ignore progress during the beginning
|
||||
positionTicks = 0;
|
||||
}
|
||||
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
||||
else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
||||
{
|
||||
// mark as completed close to the end
|
||||
positionTicks = 0;
|
||||
|
||||
@@ -82,11 +82,6 @@ namespace Emby.Server.Implementations.MediaEncoder
|
||||
return false;
|
||||
}
|
||||
|
||||
if (video.VideoType == VideoType.Dvd)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (video.IsShortcut)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@@ -368,7 +369,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
|
||||
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
|
||||
{
|
||||
if (packageInfo == null)
|
||||
{
|
||||
@@ -411,9 +412,9 @@ namespace Emby.Server.Implementations.Plugins
|
||||
Overview = packageInfo.Overview,
|
||||
Owner = packageInfo.Owner,
|
||||
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
|
||||
Version = versionInfo.Version,
|
||||
Status = PluginStatus.Active,
|
||||
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
|
||||
AutoUpdate = true,
|
||||
ImagePath = imagePath
|
||||
};
|
||||
|
||||
@@ -1543,23 +1543,26 @@ namespace Emby.Server.Implementations.Session
|
||||
Limit = 1
|
||||
}).Items.FirstOrDefault();
|
||||
|
||||
var allExistingForDevice = _authRepo.Get(
|
||||
new AuthenticationInfoQuery
|
||||
{
|
||||
DeviceId = deviceId
|
||||
}).Items;
|
||||
|
||||
foreach (var auth in allExistingForDevice)
|
||||
if (!string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
|
||||
var allExistingForDevice = _authRepo.Get(
|
||||
new AuthenticationInfoQuery
|
||||
{
|
||||
DeviceId = deviceId
|
||||
}).Items;
|
||||
|
||||
foreach (var auth in allExistingForDevice)
|
||||
{
|
||||
try
|
||||
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
|
||||
{
|
||||
Logout(auth);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while logging out.");
|
||||
try
|
||||
{
|
||||
Logout(auth);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while logging out.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
|
||||
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
|
||||
}
|
||||
|
||||
private static int GetSpecialCompareValue(Episode item)
|
||||
private static long GetSpecialCompareValue(Episode item)
|
||||
{
|
||||
// First sort by season number
|
||||
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
|
||||
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
|
||||
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
|
||||
|
||||
// Second sort order is if it airs after the season
|
||||
if (item.AirsAfterSeasonNumber.HasValue)
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
||||
_sessionManager.SessionEnded += OnSessionEnded;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -352,18 +352,18 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
return;
|
||||
}
|
||||
|
||||
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
||||
_sessionManager.SessionEnded -= OnSessionEnded;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
||||
private void OnSessionEnded(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
|
||||
{
|
||||
var request = new JoinGroupRequest(group.GroupId);
|
||||
JoinGroup(session, request, CancellationToken.None);
|
||||
var leaveGroupRequest = new LeaveGroupRequest();
|
||||
LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -11,7 +10,6 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||
@@ -23,12 +21,14 @@ namespace Emby.Server.Implementations.TV
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
|
||||
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
|
||||
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
_libraryManager = libraryManager;
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
|
||||
@@ -200,13 +200,10 @@ namespace Emby.Server.Implementations.TV
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = new ItemFields[]
|
||||
{
|
||||
ItemFields.SortName
|
||||
},
|
||||
Fields = new[] { ItemFields.SortName },
|
||||
EnableImages = false
|
||||
}
|
||||
}).FirstOrDefault();
|
||||
}).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
Func<Episode> getEpisode = () =>
|
||||
{
|
||||
@@ -224,6 +221,43 @@ namespace Emby.Server.Implementations.TV
|
||||
DtoOptions = dtoOptions
|
||||
}).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||
{
|
||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
ParentIndexNumber = 0,
|
||||
IncludeItemTypes = new[] { nameof(Episode) },
|
||||
IsPlayed = false,
|
||||
IsVirtualItem = false,
|
||||
DtoOptions = dtoOptions
|
||||
})
|
||||
.Cast<Episode>()
|
||||
.Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
|
||||
.ToList();
|
||||
|
||||
if (lastWatchedEpisode != null)
|
||||
{
|
||||
// Last watched episode is added, because there could be specials that aired before the last watched episode
|
||||
consideredEpisodes.Add(lastWatchedEpisode);
|
||||
}
|
||||
|
||||
if (nextEpisode != null)
|
||||
{
|
||||
consideredEpisodes.Add(nextEpisode);
|
||||
}
|
||||
|
||||
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
|
||||
.Cast<Episode>();
|
||||
if (lastWatchedEpisode != null)
|
||||
{
|
||||
sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
|
||||
}
|
||||
|
||||
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (nextEpisode != null)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
||||
|
||||
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Events.Updates;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
|
||||
if (plugin != null)
|
||||
{
|
||||
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
|
||||
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Remove versions with a target ABI greater then the current application version.
|
||||
@@ -500,7 +501,8 @@ namespace Emby.Server.Implementations.Updates
|
||||
var plugins = _pluginManager.Plugins;
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
if (plugin.Manifest?.AutoUpdate == false)
|
||||
// Don't auto update when plugin marked not to, or when it's disabled.
|
||||
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -515,7 +517,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
|
||||
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = Path.GetExtension(package.SourceUrl);
|
||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -567,7 +569,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
stream.Position = 0;
|
||||
_zipClient.ExtractAllFromZip(stream, targetDir, true);
|
||||
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
|
||||
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
|
||||
_pluginManager.ImportPluginFrom(targetDir);
|
||||
}
|
||||
|
||||
@@ -576,7 +578,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
|
||||
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
|
||||
|
||||
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
|
||||
await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
||||
|
||||
return plugin != null;
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -177,13 +177,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -342,13 +342,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var streamingRequest = new HlsVideoRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -250,7 +250,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -259,13 +259,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -393,7 +393,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var streamingRequest = new HlsAudioRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -417,7 +417,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -426,13 +426,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -556,7 +556,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var streamingRequest = new VideoRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -580,7 +580,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -589,13 +589,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -721,7 +721,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var streamingRequest = new StreamingRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -745,7 +745,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -754,13 +754,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -891,7 +891,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -915,7 +915,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -924,13 +924,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -1063,7 +1063,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -1087,7 +1087,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -1096,13 +1096,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal))
|
||||
{
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
@@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal) || Path.GetExtension(file) != ".m3u8")
|
||||
{
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
@@ -146,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.Ordinal))
|
||||
{
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!path.StartsWith(_applicationPaths.GeneralPath))
|
||||
if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.Ordinal))
|
||||
{
|
||||
return BadRequest("Invalid image path.");
|
||||
}
|
||||
@@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
if (!path.StartsWith(basePath))
|
||||
if (!path.StartsWith(basePath, StringComparison.Ordinal))
|
||||
{
|
||||
return BadRequest("Invalid image path.");
|
||||
}
|
||||
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
if (!path.StartsWith(basePath))
|
||||
if (!path.StartsWith(basePath, StringComparison.Ordinal))
|
||||
{
|
||||
return BadRequest("Invalid image path.");
|
||||
}
|
||||
|
||||
@@ -480,6 +480,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||
@@ -509,6 +511,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] ImageFormat? format,
|
||||
@@ -539,6 +543,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -560,6 +566,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||
@@ -589,6 +597,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] ImageFormat? format,
|
||||
@@ -618,6 +628,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -638,6 +650,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
@@ -667,6 +681,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromRoute, Required] string tag,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromRoute, Required] ImageFormat format,
|
||||
@@ -697,6 +713,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -721,6 +739,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -750,6 +770,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -776,6 +798,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -800,6 +824,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -829,6 +855,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -855,6 +883,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -880,6 +910,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -909,6 +941,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -934,6 +968,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -958,6 +994,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -987,6 +1025,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1013,6 +1053,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1038,6 +1080,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1067,6 +1111,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1092,6 +1138,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1116,6 +1164,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1145,6 +1195,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1171,6 +1223,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1196,6 +1250,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1225,6 +1281,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1250,6 +1308,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1274,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1303,6 +1365,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1329,6 +1393,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1354,6 +1420,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1383,6 +1451,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1408,6 +1478,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1432,6 +1504,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1461,6 +1535,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1504,6 +1580,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1530,6 +1608,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="fillWidth">Width of box to fill.</param>
|
||||
/// <param name="fillHeight">Height of box to fill.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
@@ -1559,6 +1639,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] int? fillWidth,
|
||||
[FromQuery] int? fillHeight,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
@@ -1601,6 +1683,8 @@ namespace Jellyfin.Api.Controllers
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
@@ -1685,6 +1769,8 @@ namespace Jellyfin.Api.Controllers
|
||||
int? width,
|
||||
int? height,
|
||||
int? quality,
|
||||
int? fillWidth,
|
||||
int? fillHeight,
|
||||
bool? cropWhitespace,
|
||||
bool? addPlayedIndicator,
|
||||
int? blur,
|
||||
@@ -1748,11 +1834,13 @@ namespace Jellyfin.Api.Controllers
|
||||
item,
|
||||
itemId,
|
||||
imageIndex,
|
||||
height,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
quality,
|
||||
width,
|
||||
height,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
fillWidth,
|
||||
fillHeight,
|
||||
quality,
|
||||
addPlayedIndicator,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
@@ -1847,11 +1935,13 @@ namespace Jellyfin.Api.Controllers
|
||||
BaseItem? item,
|
||||
Guid itemId,
|
||||
int? index,
|
||||
int? height,
|
||||
int? maxHeight,
|
||||
int? maxWidth,
|
||||
int? quality,
|
||||
int? width,
|
||||
int? height,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? fillWidth,
|
||||
int? fillHeight,
|
||||
int? quality,
|
||||
bool? addPlayedIndicator,
|
||||
double? percentPlayed,
|
||||
int? unplayedCount,
|
||||
@@ -1880,6 +1970,8 @@ namespace Jellyfin.Api.Controllers
|
||||
ItemId = itemId,
|
||||
MaxHeight = maxHeight,
|
||||
MaxWidth = maxWidth,
|
||||
FillHeight = fillHeight,
|
||||
FillWidth = fillWidth,
|
||||
Quality = quality ?? 100,
|
||||
Width = width,
|
||||
AddPlayedIndicator = addPlayedIndicator ?? false,
|
||||
|
||||
@@ -239,48 +239,6 @@ namespace Jellyfin.Api.Controllers
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <param name="providerName">The provider name.</param>
|
||||
/// <response code="200">Remote image retrieved.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
|
||||
/// </returns>
|
||||
[HttpGet("Items/RemoteSearch/Image")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteSearchImage(
|
||||
[FromQuery, Required] string imageUrl,
|
||||
[FromQuery, Required] string providerName)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
|
||||
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies search criteria to an item and refreshes metadata.
|
||||
/// </summary>
|
||||
@@ -322,54 +280,5 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the image.
|
||||
/// </summary>
|
||||
/// <param name="providerName">Name of the provider.</param>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="urlHash">The URL hash.</param>
|
||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
|
||||
if (result.Content.Headers.ContentType?.MediaType == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
|
||||
}
|
||||
|
||||
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var stream = result.Content)
|
||||
{
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
await using var fileStream = new FileStream(
|
||||
fullCachePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
true);
|
||||
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||
|
||||
Directory.CreateDirectory(pointerCacheDirectory);
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full cache path.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetFullCachePath(string filename)
|
||||
=> Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,8 +247,13 @@ namespace Jellyfin.Api.Controllers
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
if (folder is IHasCollectionType hasCollectionType
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
string? collectionType = null;
|
||||
if (folder is IHasCollectionType hasCollectionType)
|
||||
{
|
||||
collectionType = hasCollectionType.CollectionType;
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
recursive = true;
|
||||
includeItemTypes = new[] { "Playlist" };
|
||||
@@ -271,10 +276,11 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
if (!(item is UserRootFolder)
|
||||
if (item is not UserRootFolder
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels))
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels)
|
||||
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
|
||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// TODO determine non-ASCII validity.
|
||||
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
|
||||
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -145,58 +145,6 @@ namespace Jellyfin.Api.Controllers
|
||||
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <response code="200">Remote image returned.</response>
|
||||
/// <response code="404">Remote image not found.</response>
|
||||
/// <returns>Image Stream.</returns>
|
||||
[HttpGet("Images/Remote")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
|
||||
{
|
||||
var urlHash = imageUrl.ToString().GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
string? contentPath = null;
|
||||
var hasFile = false;
|
||||
|
||||
try
|
||||
{
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
hasFile = true;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
|
||||
if (!hasFile)
|
||||
{
|
||||
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(contentPath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(contentPath);
|
||||
return PhysicalFile(contentPath, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a remote image for an item.
|
||||
/// </summary>
|
||||
|
||||
@@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
|
||||
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
|
||||
/// <param name="startPositionTicks">The starting position of the first item.</param>
|
||||
/// <param name="mediaSourceId">Optional. The media source id.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param>
|
||||
/// <param name="startIndex">Optional. The start index.</param>
|
||||
/// <response code="204">Instruction sent to session.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Sessions/{sessionId}/Playing")]
|
||||
@@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] PlayCommand playCommand,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
||||
[FromQuery] long? startPositionTicks)
|
||||
[FromQuery] long? startPositionTicks,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] int? startIndex)
|
||||
{
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = itemIds,
|
||||
StartPositionTicks = startPositionTicks,
|
||||
PlayCommand = playCommand
|
||||
PlayCommand = playCommand,
|
||||
MediaSourceId = mediaSourceId,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
StartIndex = startIndex
|
||||
};
|
||||
|
||||
_sessionManager.SendPlayCommand(
|
||||
@@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Issues a command to a client to display a message to the user.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session id.</param>
|
||||
/// <param name="text">The message test.</param>
|
||||
/// <param name="header">The message header.</param>
|
||||
/// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
|
||||
/// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param>
|
||||
/// <response code="204">Message sent.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Sessions/{sessionId}/Message")]
|
||||
@@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendMessageCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] string text,
|
||||
[FromQuery] string? header,
|
||||
[FromQuery] long? timeoutMs)
|
||||
[FromBody, Required] MessageCommand command)
|
||||
{
|
||||
var command = new MessageCommand
|
||||
if (string.IsNullOrWhiteSpace(command.Header))
|
||||
{
|
||||
Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
|
||||
TimeoutMs = timeoutMs,
|
||||
Text = text
|
||||
};
|
||||
command.Header = "Message from Server";
|
||||
}
|
||||
|
||||
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
|
||||
|
||||
|
||||
@@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets subtitles in a specified format.
|
||||
/// </summary>
|
||||
/// <param name="routeItemId">The (route) item id.</param>
|
||||
/// <param name="routeMediaSourceId">The (route) media source id.</param>
|
||||
/// <param name="routeIndex">The (route) subtitle stream index.</param>
|
||||
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
@@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
||||
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
[FromRoute, Required] int index,
|
||||
[FromRoute, Required] string format,
|
||||
[FromRoute, Required] Guid routeItemId,
|
||||
[FromRoute, Required] string routeMediaSourceId,
|
||||
[FromRoute, Required] int routeIndex,
|
||||
[FromRoute, Required] string routeFormat,
|
||||
[FromQuery, ParameterObsolete] Guid? itemId,
|
||||
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||
[FromQuery, ParameterObsolete] int? index,
|
||||
[FromQuery, ParameterObsolete] string? format,
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false,
|
||||
[FromQuery] long startPositionTicks = 0)
|
||||
{
|
||||
// Set parameters to route value if not provided via query.
|
||||
itemId ??= routeItemId;
|
||||
mediaSourceId ??= routeMediaSourceId;
|
||||
index ??= routeIndex;
|
||||
format ??= routeFormat;
|
||||
|
||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
format = "json";
|
||||
@@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(itemId);
|
||||
var item = (Video)_libraryManager.GetItemById(itemId.Value);
|
||||
|
||||
var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
|
||||
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
|
||||
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
|
||||
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
|
||||
|
||||
@@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
||||
{
|
||||
await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
|
||||
await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
@@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
return File(
|
||||
await EncodeSubtitles(
|
||||
itemId,
|
||||
itemId.Value,
|
||||
mediaSourceId,
|
||||
index,
|
||||
index.Value,
|
||||
format,
|
||||
startPositionTicks,
|
||||
endPositionTicks,
|
||||
@@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets subtitles in a specified format.
|
||||
/// </summary>
|
||||
/// <param name="routeItemId">The (route) item id.</param>
|
||||
/// <param name="routeMediaSourceId">The (route) media source id.</param>
|
||||
/// <param name="routeIndex">The (route) subtitle stream index.</param>
|
||||
/// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param>
|
||||
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
||||
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
|
||||
/// <param name="format">The format of the returned subtitle.</param>
|
||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
|
||||
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public Task<ActionResult> GetSubtitleWithTicks(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
[FromRoute, Required] int index,
|
||||
[FromRoute, Required] long startPositionTicks,
|
||||
[FromRoute, Required] string format,
|
||||
[FromRoute, Required] Guid routeItemId,
|
||||
[FromRoute, Required] string routeMediaSourceId,
|
||||
[FromRoute, Required] int routeIndex,
|
||||
[FromRoute, Required] long routeStartPositionTicks,
|
||||
[FromRoute, Required] string routeFormat,
|
||||
[FromQuery, ParameterObsolete] Guid? itemId,
|
||||
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||
[FromQuery, ParameterObsolete] int? index,
|
||||
[FromQuery, ParameterObsolete] long? startPositionTicks,
|
||||
[FromQuery, ParameterObsolete] string? format,
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false)
|
||||
{
|
||||
return GetSubtitle(
|
||||
routeItemId,
|
||||
routeMediaSourceId,
|
||||
routeIndex,
|
||||
routeFormat,
|
||||
itemId,
|
||||
mediaSourceId,
|
||||
index,
|
||||
@@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers
|
||||
endPositionTicks,
|
||||
copyTimestamps,
|
||||
addVttTimeMap,
|
||||
startPositionTicks);
|
||||
startPositionTicks ?? routeStartPositionTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||
RequireAvc = true,
|
||||
DeInterlace = true,
|
||||
RequireNonAnamorphic = true,
|
||||
EnableMpegtsM2TsMode = true,
|
||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||
RequireAvc = false,
|
||||
DeInterlace = false,
|
||||
RequireNonAnamorphic = false,
|
||||
EnableMpegtsM2TsMode = false,
|
||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||
Context = EncodingContext.Static,
|
||||
StreamOptions = new Dictionary<string, string>(),
|
||||
EnableAdaptiveBitrateStreaming = true
|
||||
@@ -251,7 +251,7 @@ namespace Jellyfin.Api.Controllers
|
||||
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = maxAudioChannels,
|
||||
CopyTimestamps = true,
|
||||
CopyTimestamps = false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleMethod = SubtitleDeliveryMethod.Embed,
|
||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -248,7 +248,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -257,13 +257,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
|
||||
@@ -386,7 +386,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
@@ -410,7 +410,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
CopyTimestamps = copyTimestamps ?? false,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
@@ -419,13 +419,13 @@ namespace Jellyfin.Api.Controllers
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
RequireAvc = requireAvc ?? false,
|
||||
DeInterlace = deInterlace ?? false,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodeReasons,
|
||||
@@ -538,7 +538,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -567,7 +567,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -581,8 +581,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[HttpGet("{itemId}/stream.{container}")]
|
||||
[HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
|
||||
@@ -46,7 +46,8 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return new FileContentResult(Array.Empty<byte>(), contentType);
|
||||
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
|
||||
@@ -68,10 +69,10 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
httpContext.Response.ContentType = contentType;
|
||||
|
||||
// if the request is a head request, return a NoContent result with the same headers as it would with a GET request
|
||||
// if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return new NoContentResult();
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
|
||||
|
||||
@@ -282,6 +282,7 @@ namespace Jellyfin.Api.Helpers
|
||||
if (streamInfo != null)
|
||||
{
|
||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,7 +308,7 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
||||
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
||||
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
||||
&& user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
||||
{
|
||||
options.ForceDirectStream = true;
|
||||
}
|
||||
@@ -326,6 +327,7 @@ namespace Jellyfin.Api.Helpers
|
||||
if (streamInfo != null)
|
||||
{
|
||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +355,7 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
// Do this after the above so that StartPositionTicks is set
|
||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -390,6 +393,7 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
// Do this after the above so that StartPositionTicks is set
|
||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.7.1</VersionPrefix>
|
||||
<VersionPrefix>10.7.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -165,7 +165,7 @@ namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
result.AddItem(item);
|
||||
result.AddItem(item, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace Jellyfin.Networking.Manager
|
||||
if (_bindExclusions.Count > 0)
|
||||
{
|
||||
// Return all the interfaces except the ones specifically excluded.
|
||||
return _interfaceAddresses.Exclude(_bindExclusions);
|
||||
return _interfaceAddresses.Exclude(_bindExclusions, false);
|
||||
}
|
||||
|
||||
if (individualInterfaces)
|
||||
@@ -310,7 +310,7 @@ namespace Jellyfin.Networking.Manager
|
||||
}
|
||||
|
||||
// Remove any excluded bind interfaces.
|
||||
return _bindAddresses.Exclude(_bindExclusions);
|
||||
return _bindAddresses.Exclude(_bindExclusions, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -397,15 +397,26 @@ namespace Jellyfin.Networking.Manager
|
||||
}
|
||||
|
||||
// Get the first LAN interface address that isn't a loopback.
|
||||
var interfaces = CreateCollection(_interfaceAddresses
|
||||
.Exclude(_bindExclusions)
|
||||
.Where(IsInLocalNetwork)
|
||||
.OrderBy(p => p.Tag));
|
||||
var interfaces = CreateCollection(
|
||||
_interfaceAddresses
|
||||
.Exclude(_bindExclusions, false)
|
||||
.Where(IsInLocalNetwork)
|
||||
.OrderBy(p => p.Tag));
|
||||
|
||||
if (interfaces.Count > 0)
|
||||
{
|
||||
if (haveSource)
|
||||
{
|
||||
foreach (var intf in interfaces)
|
||||
{
|
||||
if (intf.Address.Equals(source.Address))
|
||||
{
|
||||
result = FormatIP6String(intf.Address);
|
||||
_logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Does the request originate in one of the interface subnets?
|
||||
// (For systems with multiple internal network cards, and multiple subnets)
|
||||
foreach (var intf in interfaces)
|
||||
@@ -532,10 +543,10 @@ namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
return _lanSubnets.Exclude(_excludedSubnets).AsNetworks();
|
||||
return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks();
|
||||
}
|
||||
|
||||
return _lanSubnets.Exclude(filter);
|
||||
return _lanSubnets.Exclude(filter, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -566,7 +577,7 @@ namespace Jellyfin.Networking.Manager
|
||||
&& ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
|| (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
|
||||
{
|
||||
result.AddItem(iface);
|
||||
result.AddItem(iface, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +587,29 @@ namespace Jellyfin.Networking.Manager
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasRemoteAccess(IPAddress remoteIp)
|
||||
{
|
||||
var config = _configurationManager.GetNetworkConfiguration();
|
||||
if (config.EnableRemoteAccess)
|
||||
{
|
||||
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
||||
// If left blank, all remote addresses will be allowed.
|
||||
if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
// remoteAddressFilter is a whitelist or blacklist.
|
||||
return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist;
|
||||
}
|
||||
}
|
||||
else if (!IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
// Remote not enabled. So everyone should be LAN.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads all settings and re-initialises the instance.
|
||||
/// </summary>
|
||||
@@ -610,8 +644,8 @@ namespace Jellyfin.Networking.Manager
|
||||
var address = IPNetAddress.Parse(parts[0]);
|
||||
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||
address.Tag = index;
|
||||
_interfaceAddresses.AddItem(address);
|
||||
_interfaceNames.Add(parts[2], Math.Abs(index));
|
||||
_interfaceAddresses.AddItem(address, false);
|
||||
_interfaceNames[parts[2]] = Math.Abs(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,7 +1062,7 @@ namespace Jellyfin.Networking.Manager
|
||||
|
||||
_logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString());
|
||||
_logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString());
|
||||
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString());
|
||||
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,7 +1116,7 @@ namespace Jellyfin.Networking.Manager
|
||||
nw.Tag *= -1;
|
||||
}
|
||||
|
||||
_interfaceAddresses.AddItem(nw);
|
||||
_interfaceAddresses.AddItem(nw, false);
|
||||
|
||||
// Store interface name so we can use the name in Collections.
|
||||
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
|
||||
@@ -1103,7 +1137,7 @@ namespace Jellyfin.Networking.Manager
|
||||
nw.Tag *= -1;
|
||||
}
|
||||
|
||||
_interfaceAddresses.AddItem(nw);
|
||||
_interfaceAddresses.AddItem(nw, false);
|
||||
|
||||
// Store interface name so we can use the name in Collections.
|
||||
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
|
||||
@@ -1137,10 +1171,10 @@ namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
_logger.LogWarning("No interfaces information available. Using loopback.");
|
||||
// Last ditch attempt - use loopback address.
|
||||
_interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
|
||||
_interfaceAddresses.AddItem(IPNetAddress.IP4Loopback, false);
|
||||
if (IsIP6Enabled)
|
||||
{
|
||||
_interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
|
||||
_interfaceAddresses.AddItem(IPNetAddress.IP6Loopback, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1217,7 +1251,7 @@ namespace Jellyfin.Networking.Manager
|
||||
private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result)
|
||||
{
|
||||
result = string.Empty;
|
||||
var addresses = _bindAddresses.Exclude(_bindExclusions);
|
||||
var addresses = _bindAddresses.Exclude(_bindExclusions, false);
|
||||
|
||||
int count = addresses.Count;
|
||||
if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any)))
|
||||
@@ -1302,7 +1336,7 @@ namespace Jellyfin.Networking.Manager
|
||||
result = string.Empty;
|
||||
// Get the first WAN interface address that isn't a loopback.
|
||||
var extResult = _interfaceAddresses
|
||||
.Exclude(_bindExclusions)
|
||||
.Exclude(_bindExclusions, false)
|
||||
.Where(p => !IsInLocalNetwork(p))
|
||||
.OrderBy(p => p.Tag);
|
||||
|
||||
|
||||
520
Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
generated
Normal file
520
Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
generated
Normal file
@@ -0,0 +1,520 @@
|
||||
#pragma warning disable CS1591
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
[DbContext(typeof(JellyfinDb))]
|
||||
[Migration("20210407110544_NullableCustomPrefValue")]
|
||||
partial class NullableCustomPrefValue
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "5.0.3");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("EndHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("StartHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LogSeverity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShortOverview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ActivityLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChromecastVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DashboardTheme")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScrollDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowBackdrop")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowSidebar")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipBackwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipForwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TvHome")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DisplayPreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RememberIndexing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSorting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SortBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ViewType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Permission_Permissions_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Permission_Permissions_Guid");
|
||||
|
||||
b.ToTable("Permissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Preference_Preferences_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Preference_Preferences_Guid");
|
||||
|
||||
b.ToTable("Preferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudioLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthenticationProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisplayCollectionsView")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DisplayMissingEpisodes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EasyPassword")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableAutoLogin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableLocalPassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableNextEpisodeAutoPlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUserPreferenceAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HidePlayedInLatest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InternalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("InvalidLoginAttemptCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastActivityDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLoginDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LoginAttemptsBeforeLockout")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordResetProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PlayDefaultAudioTrack")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberAudioSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSubtitleSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RemoteClientBitrateLimit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SubtitleLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SubtitleMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SyncPlayAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("AccessSchedules")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("DisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
|
||||
.WithMany("HomeSections")
|
||||
.HasForeignKey("DisplayPreferencesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithOne("ProfileImage")
|
||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("ItemDisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("Permission_Permissions_Guid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Preferences")
|
||||
.HasForeignKey("Preference_Preferences_Guid");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Navigation("HomeSections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("AccessSchedules");
|
||||
|
||||
b.Navigation("DisplayPreferences");
|
||||
|
||||
b.Navigation("ItemDisplayPreferences");
|
||||
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("Preferences");
|
||||
|
||||
b.Navigation("ProfileImage");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma warning disable CS1591
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
public partial class NullableCustomPrefValue : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
schema: "jellyfin",
|
||||
table: "CustomItemDisplayPreferences",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
schema: "jellyfin",
|
||||
table: "CustomItemDisplayPreferences",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "5.0.0");
|
||||
.HasAnnotation("ProductVersion", "5.0.3");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -110,7 +110,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
@@ -448,8 +447,8 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithOne("DisplayPreferences")
|
||||
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
|
||||
.WithMany("DisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
@@ -502,8 +501,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
b.Navigation("AccessSchedules");
|
||||
|
||||
b.Navigation("DisplayPreferences")
|
||||
.IsRequired();
|
||||
b.Navigation("DisplayPreferences");
|
||||
|
||||
b.Navigation("ItemDisplayPreferences");
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
|
||||
public Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
|
||||
{
|
||||
return _dbContext.CustomItemDisplayPreferences
|
||||
.AsQueryable()
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace Jellyfin.Server.Extensions
|
||||
{
|
||||
return serviceCollection.AddSwaggerGen(c =>
|
||||
{
|
||||
var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString() ?? "0.0.0.1";
|
||||
var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
|
||||
c.SwaggerDoc("api-docs", new OpenApiInfo
|
||||
{
|
||||
Title = "Jellyfin API",
|
||||
@@ -319,7 +319,7 @@ namespace Jellyfin.Server.Extensions
|
||||
c.OperationFilter<FileResponseFilter>();
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
c.OperationFilter<ParameterObsoleteFilter>();
|
||||
c.DocumentFilter<WebsocketModelFilter>();
|
||||
c.DocumentFilter<AdditionalModelFilter>();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.ApiClient;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
@@ -9,9 +10,9 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
namespace Jellyfin.Server.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Add models used in websocket messaging.
|
||||
/// Add models not directly used by the API, but used for discovery and websockets.
|
||||
/// </summary>
|
||||
public class WebsocketModelFilter : IDocumentFilter
|
||||
public class AdditionalModelFilter : IDocumentFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
@@ -27,6 +28,7 @@ namespace Jellyfin.Server.Filters
|
||||
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
|
||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<AssemblyName>jellyfin</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
||||
@@ -29,9 +29,8 @@ namespace Jellyfin.Server.Middleware
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The current HTTP context.</param>
|
||||
/// <param name="networkManager">The network manager.</param>
|
||||
/// <param name="serverConfigurationManager">The server configuration manager.</param>
|
||||
/// <returns>The async task.</returns>
|
||||
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
|
||||
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager)
|
||||
{
|
||||
if (httpContext.IsLocal())
|
||||
{
|
||||
@@ -42,32 +41,8 @@ namespace Jellyfin.Server.Middleware
|
||||
|
||||
var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
|
||||
|
||||
if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
|
||||
if (!networkManager.HasRemoteAccess(remoteIp))
|
||||
{
|
||||
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
||||
// If left blank, all remote addresses will be allowed.
|
||||
var remoteAddressFilter = networkManager.RemoteAddressFilter;
|
||||
|
||||
if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
// remoteAddressFilter is a whitelist or blacklist.
|
||||
bool isListed = remoteAddressFilter.ContainsAddress(remoteIp);
|
||||
if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist)
|
||||
{
|
||||
// Black list, so flip over.
|
||||
isListed = !isListed;
|
||||
}
|
||||
|
||||
if (!isListed)
|
||||
{
|
||||
// If your name isn't on the list, you arn't coming in.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
// Remote not enabled. So everyone should be LAN.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename);
|
||||
using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null);
|
||||
_logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType");
|
||||
connection.Execute("CREATE INDEX idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
|
||||
connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
|
||||
_logger.LogInformation("Creating index idx_PeopleNameListOrder");
|
||||
connection.Execute("CREATE INDEX idx_PeopleNameListOrder ON People(Name, ListOrder);");
|
||||
connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,13 +126,13 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
ShowSidebar = dto.ShowSidebar,
|
||||
ScrollDirection = dto.ScrollDirection,
|
||||
ChromecastVersion = chromecastVersion,
|
||||
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
|
||||
? int.Parse(length, CultureInfo.InvariantCulture)
|
||||
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength)
|
||||
? skipForwardLength
|
||||
: 30000,
|
||||
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
|
||||
? int.Parse(length, CultureInfo.InvariantCulture)
|
||||
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength)
|
||||
? skipBackwardLength
|
||||
: 10000,
|
||||
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
|
||||
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
|
||||
? bool.Parse(enabled)
|
||||
: true,
|
||||
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.7.1</VersionPrefix>
|
||||
<VersionPrefix>10.7.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -229,5 +229,12 @@ namespace MediaBrowser.Common.Net
|
||||
/// <param name="filter">Optional filter for the list.</param>
|
||||
/// <returns>Returns a filtered list of LAN addresses.</returns>
|
||||
Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if <paramref name="remoteIp"/> has access.
|
||||
/// </summary>
|
||||
/// <param name="remoteIp">IP Address of client.</param>
|
||||
/// <returns><b>True</b> if has access, otherwise <b>false</b>.</returns>
|
||||
bool HasRemoteAccess(IPAddress remoteIp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ namespace MediaBrowser.Common.Net
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="item">Item to add.</param>
|
||||
public static void AddItem(this Collection<IPObject> source, IPObject item)
|
||||
/// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets.
|
||||
/// If <b>false</b> items are addresses.</param>
|
||||
public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true)
|
||||
{
|
||||
if (!source.ContainsAddress(item))
|
||||
if (!source.ContainsAddress(item) || !itemsAreNetworks)
|
||||
{
|
||||
source.Add(item);
|
||||
}
|
||||
@@ -195,8 +197,9 @@ namespace MediaBrowser.Common.Net
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="excludeList">Items to exclude.</param>
|
||||
/// <param name="isNetwork">Collection is a network collection.</param>
|
||||
/// <returns>A new collection, with the items excluded.</returns>
|
||||
public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList)
|
||||
public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork)
|
||||
{
|
||||
if (source.Count == 0 || excludeList == null)
|
||||
{
|
||||
@@ -221,7 +224,7 @@ namespace MediaBrowser.Common.Net
|
||||
|
||||
if (!found)
|
||||
{
|
||||
results.AddItem(outer);
|
||||
results.AddItem(outer, isNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -51,8 +52,9 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
|
||||
/// <param name="version">Version to be installed.</param>
|
||||
/// <param name="path">The path where to save the manifest.</param>
|
||||
/// <param name="status">Initial status of the plugin.</param>
|
||||
/// <returns>True if successful.</returns>
|
||||
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path);
|
||||
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Imports plugin details from a folder.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -9,67 +10,12 @@ namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public static class ImageHelper
|
||||
{
|
||||
public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize)
|
||||
public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize)
|
||||
{
|
||||
if (originalImageSize.HasValue)
|
||||
{
|
||||
// Determine the output size based on incoming parameters
|
||||
var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
|
||||
|
||||
return newSize;
|
||||
}
|
||||
|
||||
return GetSizeEstimate(options);
|
||||
}
|
||||
|
||||
private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
|
||||
{
|
||||
if (options.Width.HasValue && options.Height.HasValue)
|
||||
{
|
||||
return new ImageDimensions(options.Width.Value, options.Height.Value);
|
||||
}
|
||||
|
||||
double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
|
||||
|
||||
int? width = options.Width ?? options.MaxWidth;
|
||||
|
||||
if (width.HasValue)
|
||||
{
|
||||
int heightValue = Convert.ToInt32((double)width.Value / aspect);
|
||||
return new ImageDimensions(width.Value, heightValue);
|
||||
}
|
||||
|
||||
var height = options.Height ?? options.MaxHeight ?? 200;
|
||||
int widthValue = Convert.ToInt32(aspect * height);
|
||||
return new ImageDimensions(widthValue, height);
|
||||
}
|
||||
|
||||
private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ImageType.Art:
|
||||
case ImageType.Backdrop:
|
||||
case ImageType.Chapter:
|
||||
case ImageType.Screenshot:
|
||||
case ImageType.Thumb:
|
||||
return 1.78;
|
||||
case ImageType.Banner:
|
||||
return 5.4;
|
||||
case ImageType.Box:
|
||||
case ImageType.BoxRear:
|
||||
case ImageType.Disc:
|
||||
case ImageType.Menu:
|
||||
case ImageType.Profile:
|
||||
return 1;
|
||||
case ImageType.Logo:
|
||||
return 2.58;
|
||||
case ImageType.Primary:
|
||||
double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
||||
return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
// Determine the output size based on incoming parameters
|
||||
var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
|
||||
newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
|
||||
return newSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace MediaBrowser.Controller.Drawing
|
||||
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
public int? FillWidth { get; set; }
|
||||
|
||||
public int? FillHeight { get; set; }
|
||||
|
||||
public int Quality { get; set; }
|
||||
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
|
||||
@@ -95,6 +99,11 @@ namespace MediaBrowser.Controller.Drawing
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1434,9 +1434,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
var linkedChildren = LinkedChildren;
|
||||
foreach (var i in linkedChildren)
|
||||
{
|
||||
if (i.ItemId.HasValue && i.ItemId.Value == itemId)
|
||||
if (i.ItemId.HasValue)
|
||||
{
|
||||
return true;
|
||||
if (i.ItemId.Value == itemId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var child = GetLinkedChild(i);
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace MediaBrowser.Controller
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="client">The client string.</param>
|
||||
/// <returns>The dictionary of custom item display preferences.</returns>
|
||||
IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
|
||||
Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the custom item display preference for the user and client.
|
||||
|
||||
@@ -466,6 +466,15 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <param name="people">The people.</param>
|
||||
void UpdatePeople(BaseItem item, List<PersonInfo> people);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously updates the people.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="people">The people.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The async task.</returns>
|
||||
Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item ids.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.7.1</VersionPrefix>
|
||||
<VersionPrefix>10.7.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -313,6 +313,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return null;
|
||||
}
|
||||
|
||||
// ISO files don't have an ffmpeg format
|
||||
if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -592,7 +598,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
&& string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
&& isNvdecDecoder)
|
||||
{
|
||||
arg.Append("-hwaccel_output_format cuda ");
|
||||
// Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
|
||||
arg.Append("-hwaccel_output_format cuda -extra_hw_frames 3 -autorotate 0 ");
|
||||
}
|
||||
|
||||
if (state.IsVideoRequest
|
||||
@@ -1066,7 +1073,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
||||
{
|
||||
// following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead.
|
||||
switch (encodingOptions.EncoderPreset)
|
||||
{
|
||||
case "veryslow":
|
||||
@@ -1247,7 +1253,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
&& profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
|
||||
&& profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profile = "constrained_baseline";
|
||||
}
|
||||
|
||||
@@ -273,6 +273,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public int? GetRequestedAudioChannels(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "audiochannels");
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (BaseRequest.MaxAudioChannels.HasValue)
|
||||
{
|
||||
return BaseRequest.MaxAudioChannels;
|
||||
@@ -288,16 +298,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return BaseRequest.TranscodingMaxAudioChannels;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "audiochannels");
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
@@ -43,7 +44,8 @@ namespace MediaBrowser.Controller.Playlists
|
||||
|
||||
public static bool IsPlaylistFile(string path)
|
||||
{
|
||||
return System.IO.Path.HasExtension(path);
|
||||
// The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
|
||||
return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -12,11 +12,11 @@ namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new ConcurrentDictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new (StringComparer.Ordinal);
|
||||
|
||||
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new (StringComparer.Ordinal);
|
||||
|
||||
private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new (StringComparer.Ordinal);
|
||||
|
||||
public DirectoryService(IFileSystem fileSystem)
|
||||
{
|
||||
|
||||
@@ -370,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
|
||||
{
|
||||
var prefix = "file";
|
||||
if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso)
|
||||
if (mediaSource.VideoType == VideoType.BluRay)
|
||||
{
|
||||
prefix = "bluray";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
@@ -85,12 +86,14 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
var val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
if (DateTime.TryParse(val, out var i))
|
||||
{
|
||||
return i.ToUniversalTime();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var i))
|
||||
{
|
||||
return i.ToUniversalTime();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -131,6 +131,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
|
||||
if (isAudio)
|
||||
|
||||
@@ -185,10 +185,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
continue;
|
||||
}
|
||||
|
||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
||||
if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase))
|
||||
string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -16,5 +16,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
public IPAddress LocalIpAddress { get; set; }
|
||||
|
||||
public int LocalPort { get; set; }
|
||||
|
||||
public IPAddress RemoteIpAddress { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,52 @@ namespace MediaBrowser.Model.Drawing
|
||||
return new ImageDimensions(newWidth, newHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scale down to fill box.
|
||||
/// Returns original size if both width and height are null or zero.
|
||||
/// </summary>
|
||||
/// <param name="size">The original size object.</param>
|
||||
/// <param name="fillWidth">A new fixed width, if desired.</param>
|
||||
/// <param name="fillHeight">A new fixed height, if desired.</param>
|
||||
/// <returns>A new size object or size.</returns>
|
||||
public static ImageDimensions ResizeFill(
|
||||
ImageDimensions size,
|
||||
int? fillWidth,
|
||||
int? fillHeight)
|
||||
{
|
||||
// Return original size if input is invalid.
|
||||
if ((fillWidth == null || fillWidth == 0)
|
||||
&& (fillHeight == null || fillHeight == 0))
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
if (fillWidth == null || fillWidth == 0)
|
||||
{
|
||||
fillWidth = 1;
|
||||
}
|
||||
|
||||
if (fillHeight == null || fillHeight == 0)
|
||||
{
|
||||
fillHeight = 1;
|
||||
}
|
||||
|
||||
double widthRatio = size.Width / (double)fillWidth;
|
||||
double heightRatio = size.Height / (double)fillHeight;
|
||||
double scaleRatio = Math.Min(widthRatio, heightRatio);
|
||||
|
||||
// Clamp to current size.
|
||||
if (scaleRatio < 1)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
int newWidth = Convert.ToInt32(Math.Ceiling(size.Width / scaleRatio));
|
||||
int newHeight = Convert.ToInt32(Math.Ceiling(size.Height / scaleRatio));
|
||||
|
||||
return new ImageDimensions(newWidth, newHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new width.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.7.1</VersionPrefix>
|
||||
<VersionPrefix>10.7.6</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -95,16 +95,17 @@ namespace MediaBrowser.Model.Notifications
|
||||
{
|
||||
NotificationOption opt = GetOptions(notificationType);
|
||||
|
||||
return opt == null ||
|
||||
!opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
|
||||
return opt == null
|
||||
|| !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool IsEnabledToMonitorUser(string type, Guid userId)
|
||||
{
|
||||
NotificationOption opt = GetOptions(type);
|
||||
|
||||
return opt != null && opt.Enabled &&
|
||||
!opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
|
||||
return opt != null
|
||||
&& opt.Enabled
|
||||
&& !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool IsEnabledToSendToUser(string type, string userId, User user)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MediaBrowser.Model.Session
|
||||
{
|
||||
public class MessageCommand
|
||||
{
|
||||
public string Header { get; set; }
|
||||
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
public string Text { get; set; }
|
||||
|
||||
public long? TimeoutMs { get; set; }
|
||||
|
||||
@@ -469,6 +469,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
try
|
||||
{
|
||||
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await _providerManager.SaveImage(
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false);
|
||||
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
@@ -216,66 +216,18 @@ namespace MediaBrowser.Providers.Manager
|
||||
lookupInfo.Year = result.ProductionYear;
|
||||
}
|
||||
|
||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||
{
|
||||
if (result.Item.SupportsPeople && result.People != null)
|
||||
{
|
||||
var baseItem = result.Item;
|
||||
|
||||
LibraryManager.UpdatePeople(baseItem, result.People);
|
||||
await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false);
|
||||
await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var personsToSave = new List<BaseItem>();
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
||||
{
|
||||
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||
var saveEntity = false;
|
||||
var personEntity = LibraryManager.GetPerson(person.Name);
|
||||
foreach (var id in person.ProviderIds)
|
||||
{
|
||||
if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
personEntity.SetProviderId(id.Key, id.Value);
|
||||
saveEntity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
|
||||
{
|
||||
personEntity.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = person.ImageUrl,
|
||||
Type = ImageType.Primary
|
||||
},
|
||||
0);
|
||||
|
||||
saveEntity = true;
|
||||
itemUpdateType = ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
if (saveEntity)
|
||||
{
|
||||
personsToSave.Add(personEntity);
|
||||
await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
|
||||
}
|
||||
|
||||
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
item.AfterMetadataRefresh();
|
||||
|
||||
@@ -175,6 +175,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var movie = new Movie
|
||||
{
|
||||
Name = movieResult.Title ?? movieResult.OriginalTitle,
|
||||
OriginalTitle = movieResult.OriginalTitle,
|
||||
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
|
||||
Tagline = movieResult.Tagline,
|
||||
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
|
||||
|
||||
@@ -49,37 +49,36 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var person = (Person)item;
|
||||
var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
|
||||
if (personTmdbId > 0)
|
||||
if (!person.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
|
||||
{
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
|
||||
if (personResult?.Images?.Profiles == null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
var remoteImages = new List<RemoteImageInfo>();
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
|
||||
for (var i = 0; i < personResult.Images.Profiles.Count; i++)
|
||||
{
|
||||
var image = personResult.Images.Profiles[i];
|
||||
remoteImages.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Type = ImageType.Primary,
|
||||
Width = image.Width,
|
||||
Height = image.Height,
|
||||
Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
|
||||
Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
|
||||
});
|
||||
}
|
||||
|
||||
return remoteImages.OrderByLanguageDescending(language);
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false);
|
||||
if (personResult?.Images?.Profiles == null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count];
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
|
||||
for (var i = 0; i < personResult.Images.Profiles.Count; i++)
|
||||
{
|
||||
var image = personResult.Images.Profiles[i];
|
||||
remoteImages[i] = new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Type = ImageType.Primary,
|
||||
Width = image.Width,
|
||||
Height = image.Height,
|
||||
Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
|
||||
Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
|
||||
};
|
||||
}
|
||||
|
||||
return remoteImages.OrderByLanguageDescending(language);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
|
||||
@@ -30,11 +30,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
|
||||
if (personTmdbId <= 0)
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
|
||||
{
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (personResult != null)
|
||||
{
|
||||
@@ -51,19 +49,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
}
|
||||
|
||||
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
|
||||
result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
|
||||
if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId))
|
||||
{
|
||||
result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
|
||||
}
|
||||
|
||||
return new[] { result };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO why? Because of the old rate limit?
|
||||
if (searchInfo.IsAutomated)
|
||||
{
|
||||
// Don't hammer moviedb searching by name
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var remoteSearchResults = new List<RemoteSearchResult>();
|
||||
|
||||
@@ -121,9 +121,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
CommunityRating = Convert.ToSingle(episodeResult.VoteAverage)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
|
||||
var externalIds = episodeResult.ExternalIds;
|
||||
if (!string.IsNullOrEmpty(externalIds?.TvdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
|
||||
item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(externalIds?.ImdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(externalIds?.TvrageId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId);
|
||||
}
|
||||
|
||||
if (episodeResult.Videos?.Results != null)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Rssdp
|
||||
/// </summary>
|
||||
public sealed class DeviceAvailableEventArgs : EventArgs
|
||||
{
|
||||
public IPAddress LocalIpAddress { get; set; }
|
||||
public IPAddress RemoteIpAddress { get; set; }
|
||||
|
||||
private readonly DiscoveredSsdpDevice _DiscoveredDevice;
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace Rssdp.Infrastructure
|
||||
/// Raises the <see cref="DeviceAvailable"/> event.
|
||||
/// </summary>
|
||||
/// <seealso cref="DeviceAvailable"/>
|
||||
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress)
|
||||
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress)
|
||||
{
|
||||
if (this.IsDisposed)
|
||||
{
|
||||
@@ -220,7 +220,7 @@ namespace Rssdp.Infrastructure
|
||||
{
|
||||
handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
|
||||
{
|
||||
LocalIpAddress = localIpAddress
|
||||
RemoteIpAddress = IpAddress
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ namespace Rssdp.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress localIpAddress)
|
||||
private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IpAddress)
|
||||
{
|
||||
bool isNewDevice = false;
|
||||
lock (_Devices)
|
||||
@@ -304,17 +304,17 @@ namespace Rssdp.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
DeviceFound(device, isNewDevice, localIpAddress);
|
||||
DeviceFound(device, isNewDevice, IpAddress);
|
||||
}
|
||||
|
||||
private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress)
|
||||
private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress)
|
||||
{
|
||||
if (!NotificationTypeMatchesFilter(device))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnDeviceAvailable(device, isNewDevice, localIpAddress);
|
||||
OnDeviceAvailable(device, isNewDevice, IpAddress);
|
||||
}
|
||||
|
||||
private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
|
||||
@@ -347,7 +347,7 @@ namespace Rssdp.Infrastructure
|
||||
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
|
||||
}
|
||||
|
||||
private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress)
|
||||
private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IpAddress)
|
||||
{
|
||||
if (!message.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -367,11 +367,11 @@ namespace Rssdp.Infrastructure
|
||||
ResponseHeaders = message.Headers
|
||||
};
|
||||
|
||||
AddOrUpdateDiscoveredDevice(device, localIpAddress);
|
||||
AddOrUpdateDiscoveredDevice(device, IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress localIpAddress)
|
||||
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IpAddress)
|
||||
{
|
||||
if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
@@ -381,7 +381,7 @@ namespace Rssdp.Infrastructure
|
||||
var notificationType = GetFirstHeaderStringValue("NTS", message);
|
||||
if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
ProcessAliveNotification(message, localIpAddress);
|
||||
ProcessAliveNotification(message, IpAddress);
|
||||
}
|
||||
else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
@@ -389,7 +389,7 @@ namespace Rssdp.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessAliveNotification(HttpRequestMessage message, IPAddress localIpAddress)
|
||||
private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IpAddress)
|
||||
{
|
||||
var location = GetFirstHeaderUriValue("Location", message);
|
||||
if (location != null)
|
||||
@@ -404,7 +404,7 @@ namespace Rssdp.Infrastructure
|
||||
ResponseHeaders = message.Headers
|
||||
};
|
||||
|
||||
AddOrUpdateDiscoveredDevice(device, localIpAddress);
|
||||
AddOrUpdateDiscoveredDevice(device, IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,7 +630,7 @@ namespace Rssdp.Infrastructure
|
||||
|
||||
private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e)
|
||||
{
|
||||
ProcessNotificationMessage(e.Message, e.LocalIpAddress);
|
||||
ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.7.1")]
|
||||
[assembly: AssemblyFileVersion("10.7.1")]
|
||||
[assembly: AssemblyVersion("10.7.6")]
|
||||
[assembly: AssemblyFileVersion("10.7.6")]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Prevent generator from creating these files:
|
||||
git_push.sh
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
artifactsDirectory="${1}"
|
||||
|
||||
java -jar openapi-generator-cli.jar generate \
|
||||
--input-spec ${artifactsDirectory}/openapispec/openapi.json \
|
||||
--generator-name typescript-axios \
|
||||
--output ./apiclient/generated/typescript/axios \
|
||||
--template-dir ./apiclient/templates/typescript/axios \
|
||||
--ignore-file-override ./apiclient/.openapi-generator-ignore \
|
||||
--additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@jellyfin/client-axios",
|
||||
"version": "10.7.0{{snapshotVersion}}",
|
||||
"description": "Jellyfin api client using axios",
|
||||
"author": "Jellyfin Contributors",
|
||||
"keywords": [
|
||||
"axios",
|
||||
"typescript",
|
||||
"jellyfin"
|
||||
],
|
||||
"license": "GPL-3.0-only",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --outDir dist/",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.11.5",
|
||||
"typescript": "^3.6.4"
|
||||
}{{#npmRepository}},{{/npmRepository}}
|
||||
{{#npmRepository}}
|
||||
"publishConfig": {
|
||||
"registry": "{{npmRepository}}"
|
||||
}
|
||||
{{/npmRepository}}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.7.1"
|
||||
version: "10.7.6"
|
||||
packages:
|
||||
- debian.amd64
|
||||
- debian.arm64
|
||||
|
||||
30
debian/changelog
vendored
30
debian/changelog
vendored
@@ -1,3 +1,33 @@
|
||||
jellyfin-server (10.7.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.6
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 20 May 2021 22:06:13 -0400
|
||||
|
||||
jellyfin-server (10.7.5-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.5
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 04 May 2021 22:08:42 -0400
|
||||
|
||||
jellyfin-server (10.7.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 04 May 2021 21:15:42 -0400
|
||||
|
||||
jellyfin-server (10.7.3-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.3
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 04 May 2021 20:01:46 -0400
|
||||
|
||||
jellyfin-server (10.7.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.2
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 11 Apr 2021 14:18:22 -0400
|
||||
|
||||
jellyfin-server (10.7.1-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.7.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.1
|
||||
|
||||
5
debian/conf/jellyfin
vendored
5
debian/conf/jellyfin
vendored
@@ -33,6 +33,11 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
||||
# [OPTIONAL] run Jellyfin without the web app
|
||||
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
|
||||
|
||||
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
|
||||
# 0 = Workstation
|
||||
# 1 = Server
|
||||
#COMPlus_gcServer=1
|
||||
|
||||
#
|
||||
# SysV init/Upstart options
|
||||
#
|
||||
|
||||
2
debian/metapackage/jellyfin
vendored
2
debian/metapackage/jellyfin
vendored
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
|
||||
Standards-Version: 3.9.2
|
||||
|
||||
Package: jellyfin
|
||||
Version: 10.7.1
|
||||
Version: 10.7.6
|
||||
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
Depends: jellyfin-server, jellyfin-web
|
||||
Description: Provides the Jellyfin Free Software Media System
|
||||
|
||||
@@ -35,3 +35,7 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
|
||||
# [OPTIONAL] run Jellyfin without the web app
|
||||
#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
|
||||
|
||||
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
|
||||
# 0 = Workstation
|
||||
# 1 = Server
|
||||
#COMPlus_gcServer=1
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
%endif
|
||||
|
||||
Name: jellyfin
|
||||
Version: 10.7.1
|
||||
Version: 10.7.6
|
||||
Release: 1%{?dist}
|
||||
Summary: The Free Software Media System
|
||||
License: GPLv3
|
||||
@@ -137,6 +137,16 @@ fi
|
||||
%systemd_postun_with_restart jellyfin.service
|
||||
|
||||
%changelog
|
||||
* Thu May 20 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.6
|
||||
* Tue May 04 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.5
|
||||
* Tue May 04 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.4
|
||||
* Tue May 04 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.3
|
||||
* Sun Apr 11 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.2
|
||||
* Sun Mar 21 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.7.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.1
|
||||
* Mon Mar 08 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Emby.Server.Implementations;
|
||||
using Jellyfin.Server;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of the abstract <see cref="ApplicationHost" /> class.
|
||||
/// </summary>
|
||||
public class TestAppHost : CoreAppHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestAppHost" /> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
|
||||
public TestAppHost(
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IConfiguration startup,
|
||||
IFileSystem fileSystem,
|
||||
IServiceCollection collection)
|
||||
: base(
|
||||
applicationPaths,
|
||||
loggerFactory,
|
||||
options,
|
||||
startup,
|
||||
fileSystem,
|
||||
collection)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
|
||||
{
|
||||
foreach (var a in base.GetAssembliesWithPartsInternal())
|
||||
{
|
||||
yield return a;
|
||||
}
|
||||
|
||||
yield return typeof(TestPlugin).Assembly;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user