Merge pull request #3808 from crobibero/api-migration-merge
Merge master into api-migration
This commit is contained in:
commit
a28d00eeba
|
@ -80,7 +80,15 @@ jobs:
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: JellyfinVersion
|
||||||
|
value: 0.0.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||||
|
displayName: Set release version (stable)
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||||
|
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
displayName: 'Push Unstable Image'
|
displayName: 'Push Unstable Image'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
|
@ -105,7 +113,7 @@ jobs:
|
||||||
containerRegistry: Docker Hub
|
containerRegistry: Docker Hub
|
||||||
tags: |
|
tags: |
|
||||||
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||||
stable-$(BuildConfiguration)
|
$(JellyfinVersion)-$(BuildConfiguration)
|
||||||
|
|
||||||
- job: CollectArtifacts
|
- job: CollectArtifacts
|
||||||
displayName: 'Collect Artifacts'
|
displayName: 'Collect Artifacts'
|
||||||
|
|
|
@ -11,6 +11,7 @@ using System.Xml;
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using Emby.Dlna.Service;
|
using Emby.Dlna.Service;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
|
|
@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl
|
||||||
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
|
var mediaProfile = _profile.GetVideoMediaProfile(
|
||||||
|
streamInfo.Container,
|
||||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||||
streamInfo.TargetAudioBitrate,
|
streamInfo.TargetAudioBitrate,
|
||||||
|
|
|
@ -122,15 +122,15 @@ namespace Emby.Dlna
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||||
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
|
||||||
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
|
||||||
|
|
||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
@ -387,7 +387,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
foreach (var name in _assembly.GetManifestResourceNames())
|
foreach (var name in _assembly.GetManifestResourceNames())
|
||||||
{
|
{
|
||||||
if (!name.StartsWith(namespaceName))
|
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -406,7 +406,7 @@ namespace Emby.Dlna
|
||||||
|
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -509,7 +509,7 @@ namespace Emby.Dlna
|
||||||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
class InternalProfileInfo
|
private class InternalProfileInfo
|
||||||
{
|
{
|
||||||
internal DeviceProfileInfo Info { get; set; }
|
internal DeviceProfileInfo Info { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
|
||||||
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
|
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
|
||||||
foreach (var key in stateVariables.Keys)
|
foreach (var key in stateVariables.Keys)
|
||||||
{
|
{
|
||||||
builder.Append("<e:property>");
|
builder.Append("<e:property>")
|
||||||
builder.Append("<" + key + ">");
|
.Append('<')
|
||||||
builder.Append(stateVariables[key]);
|
.Append(key)
|
||||||
builder.Append("</" + key + ">");
|
.Append('>')
|
||||||
builder.Append("</e:property>");
|
.Append(stateVariables[key])
|
||||||
|
.Append("</")
|
||||||
|
.Append(key)
|
||||||
|
.Append('>')
|
||||||
|
.Append("</e:property>");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append("</e:propertyset>");
|
builder.Append("</e:propertyset>");
|
||||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Server;
|
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DescriptionXmlBuilder.Escape(value);
|
return SecurityElement.Escape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
foreach (var att in attributes)
|
foreach (var att in attributes)
|
||||||
{
|
{
|
||||||
builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
|
builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append(">");
|
builder.Append('>');
|
||||||
|
|
||||||
builder.Append("<specVersion>");
|
builder.Append("<specVersion>");
|
||||||
builder.Append("<major>1</major>");
|
builder.Append("<major>1</major>");
|
||||||
|
@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
if (!EnableAbsoluteUrls)
|
if (!EnableAbsoluteUrls)
|
||||||
{
|
{
|
||||||
builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
|
builder.Append("<URLBase>")
|
||||||
|
.Append(SecurityElement.Escape(_serverAddress))
|
||||||
|
.Append("</URLBase>");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppendDeviceInfo(builder);
|
AppendDeviceInfo(builder);
|
||||||
|
@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
AppendIconList(builder);
|
AppendIconList(builder);
|
||||||
|
|
||||||
builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>");
|
builder.Append("<presentationURL>")
|
||||||
|
.Append(SecurityElement.Escape(_serverAddress))
|
||||||
|
.Append("/web/index.html</presentationURL>");
|
||||||
|
|
||||||
AppendServiceList(builder);
|
AppendServiceList(builder);
|
||||||
builder.Append("</device>");
|
builder.Append("</device>");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly char[] s_escapeChars = new char[]
|
|
||||||
{
|
|
||||||
'<',
|
|
||||||
'>',
|
|
||||||
'"',
|
|
||||||
'\'',
|
|
||||||
'&'
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string[] s_escapeStringPairs = new[]
|
|
||||||
{
|
|
||||||
"<",
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
">",
|
|
||||||
"\"",
|
|
||||||
""",
|
|
||||||
"'",
|
|
||||||
"'",
|
|
||||||
"&",
|
|
||||||
"&"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string GetEscapeSequence(char c)
|
|
||||||
{
|
|
||||||
int num = s_escapeStringPairs.Length;
|
|
||||||
for (int i = 0; i < num; i += 2)
|
|
||||||
{
|
|
||||||
string text = s_escapeStringPairs[i];
|
|
||||||
string result = s_escapeStringPairs[i + 1];
|
|
||||||
if (text[0] == c)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
|
|
||||||
/// <returns>The input string with invalid characters replaced.</returns>
|
|
||||||
/// <param name="str">The string within which to escape invalid characters. </param>
|
|
||||||
public static string Escape(string str)
|
|
||||||
{
|
|
||||||
if (str == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder stringBuilder = null;
|
|
||||||
int length = str.Length;
|
|
||||||
int num = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int num2 = str.IndexOfAny(s_escapeChars, num);
|
|
||||||
if (num2 == -1)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringBuilder == null)
|
|
||||||
{
|
|
||||||
stringBuilder = new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
stringBuilder.Append(str, num, num2 - num);
|
|
||||||
stringBuilder.Append(GetEscapeSequence(str[num2]));
|
|
||||||
num = num2 + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringBuilder == null)
|
|
||||||
{
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
stringBuilder.Append(str, num, length - num);
|
|
||||||
return stringBuilder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendDeviceProperties(StringBuilder builder)
|
private void AppendDeviceProperties(StringBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Append("<dlna:X_DLNACAP/>");
|
builder.Append("<dlna:X_DLNACAP/>");
|
||||||
|
@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
|
||||||
|
|
||||||
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
|
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
|
||||||
|
|
||||||
builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
|
builder.Append("<friendlyName>")
|
||||||
builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
|
.Append(SecurityElement.Escape(GetFriendlyName()))
|
||||||
builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
|
.Append("</friendlyName>");
|
||||||
|
builder.Append("<manufacturer>")
|
||||||
|
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
|
||||||
|
.Append("</manufacturer>");
|
||||||
|
builder.Append("<manufacturerURL>")
|
||||||
|
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
|
||||||
|
.Append("</manufacturerURL>");
|
||||||
|
|
||||||
builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
|
builder.Append("<modelDescription>")
|
||||||
builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
|
.Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
|
||||||
|
.Append("</modelDescription>");
|
||||||
|
builder.Append("<modelName>")
|
||||||
|
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
|
||||||
|
.Append("</modelName>");
|
||||||
|
|
||||||
builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
|
builder.Append("<modelNumber>")
|
||||||
builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
|
.Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
|
||||||
|
.Append("</modelNumber>");
|
||||||
|
builder.Append("<modelURL>")
|
||||||
|
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
|
||||||
|
.Append("</modelURL>");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_profile.SerialNumber))
|
if (string.IsNullOrEmpty(_profile.SerialNumber))
|
||||||
{
|
{
|
||||||
builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
|
builder.Append("<serialNumber>")
|
||||||
|
.Append(SecurityElement.Escape(_serverId))
|
||||||
|
.Append("</serialNumber>");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
|
builder.Append("<serialNumber>")
|
||||||
|
.Append(SecurityElement.Escape(_profile.SerialNumber))
|
||||||
|
.Append("</serialNumber>");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append("<UPC/>");
|
builder.Append("<UPC/>");
|
||||||
|
|
||||||
builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
|
builder.Append("<UDN>uuid:")
|
||||||
|
.Append(SecurityElement.Escape(_serverUdn))
|
||||||
|
.Append("</UDN>");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
|
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
|
||||||
{
|
{
|
||||||
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
|
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
|
||||||
|
.Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
|
||||||
|
.Append("</av:aggregationFlags>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
|
||||||
{
|
{
|
||||||
builder.Append("<icon>");
|
builder.Append("<icon>");
|
||||||
|
|
||||||
builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
|
builder.Append("<mimetype>")
|
||||||
builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
|
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||||
builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
|
.Append("</mimetype>");
|
||||||
builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
|
builder.Append("<width>")
|
||||||
builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
|
.Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
|
||||||
|
.Append("</width>");
|
||||||
|
builder.Append("<height>")
|
||||||
|
.Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
|
||||||
|
.Append("</height>");
|
||||||
|
builder.Append("<depth>")
|
||||||
|
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||||
|
.Append("</depth>");
|
||||||
|
builder.Append("<url>")
|
||||||
|
.Append(BuildUrl(icon.Url))
|
||||||
|
.Append("</url>");
|
||||||
|
|
||||||
builder.Append("</icon>");
|
builder.Append("</icon>");
|
||||||
}
|
}
|
||||||
|
@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
|
||||||
{
|
{
|
||||||
builder.Append("<service>");
|
builder.Append("<service>");
|
||||||
|
|
||||||
builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
|
builder.Append("<serviceType>")
|
||||||
builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
|
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||||
builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
|
.Append("</serviceType>");
|
||||||
builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
|
builder.Append("<serviceId>")
|
||||||
builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
|
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
||||||
|
.Append("</serviceId>");
|
||||||
|
builder.Append("<SCPDURL>")
|
||||||
|
.Append(BuildUrl(service.ScpdUrl))
|
||||||
|
.Append("</SCPDURL>");
|
||||||
|
builder.Append("<controlURL>")
|
||||||
|
.Append(BuildUrl(service.ControlUrl))
|
||||||
|
.Append("</controlURL>");
|
||||||
|
builder.Append("<eventSubURL>")
|
||||||
|
.Append(BuildUrl(service.EventSubUrl))
|
||||||
|
.Append("</eventSubURL>");
|
||||||
|
|
||||||
builder.Append("</service>");
|
builder.Append("</service>");
|
||||||
}
|
}
|
||||||
|
@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
|
||||||
url = _serverAddress.TrimEnd('/') + url;
|
url = _serverAddress.TrimEnd('/') + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Escape(url);
|
return SecurityElement.Escape(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<DeviceIcon> GetIcons()
|
private IEnumerable<DeviceIcon> GetIcons()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Emby.Dlna.Common;
|
using Emby.Dlna.Common;
|
||||||
using Emby.Dlna.Server;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.Service
|
namespace Emby.Dlna.Service
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
|
||||||
{
|
{
|
||||||
builder.Append("<action>");
|
builder.Append("<action>");
|
||||||
|
|
||||||
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
|
builder.Append("<name>")
|
||||||
|
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||||
|
.Append("</name>");
|
||||||
|
|
||||||
builder.Append("<argumentList>");
|
builder.Append("<argumentList>");
|
||||||
|
|
||||||
|
@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
|
||||||
{
|
{
|
||||||
builder.Append("<argument>");
|
builder.Append("<argument>");
|
||||||
|
|
||||||
builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
|
builder.Append("<name>")
|
||||||
builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
|
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||||
builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
|
.Append("</name>");
|
||||||
|
builder.Append("<direction>")
|
||||||
|
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
|
||||||
|
.Append("</direction>");
|
||||||
|
builder.Append("<relatedStateVariable>")
|
||||||
|
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
|
||||||
|
.Append("</relatedStateVariable>");
|
||||||
|
|
||||||
builder.Append("</argument>");
|
builder.Append("</argument>");
|
||||||
}
|
}
|
||||||
|
@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
|
||||||
{
|
{
|
||||||
var sendEvents = item.SendsEvents ? "yes" : "no";
|
var sendEvents = item.SendsEvents ? "yes" : "no";
|
||||||
|
|
||||||
builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
|
builder.Append("<stateVariable sendEvents=\"")
|
||||||
|
.Append(sendEvents)
|
||||||
|
.Append("\">");
|
||||||
|
|
||||||
builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
|
builder.Append("<name>")
|
||||||
builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
|
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||||
|
.Append("</name>");
|
||||||
|
builder.Append("<dataType>")
|
||||||
|
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
||||||
|
.Append("</dataType>");
|
||||||
|
|
||||||
if (item.AllowedValues.Length > 0)
|
if (item.AllowedValues.Length > 0)
|
||||||
{
|
{
|
||||||
builder.Append("<allowedValueList>");
|
builder.Append("<allowedValueList>");
|
||||||
foreach (var allowedValue in item.AllowedValues)
|
foreach (var allowedValue in item.AllowedValues)
|
||||||
{
|
{
|
||||||
builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>");
|
builder.Append("<allowedValue>")
|
||||||
|
.Append(SecurityElement.Escape(allowedValue))
|
||||||
|
.Append("</allowedValue>");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append("</allowedValueList>");
|
builder.Append("</allowedValueList>");
|
||||||
|
|
|
@ -448,21 +448,21 @@ namespace Emby.Drawing
|
||||||
/// or
|
/// or
|
||||||
/// filename.
|
/// filename.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public string GetCachePath(string path, string filename)
|
public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (path.IsEmpty)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentException("Path can't be empty.", nameof(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (path.IsEmpty)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(filename));
|
throw new ArgumentException("Filename can't be empty.", nameof(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefix = filename.Substring(0, 1);
|
var prefix = filename.Slice(0, 1);
|
||||||
|
|
||||||
return Path.Combine(path, prefix, filename);
|
return Path.Join(path, prefix, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
@ -136,8 +136,8 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
CleanDateTimes = new[]
|
CleanDateTimes = new[]
|
||||||
{
|
{
|
||||||
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
|
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
|
||||||
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
|
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
|
||||||
};
|
};
|
||||||
|
|
||||||
CleanStrings = new[]
|
CleanStrings = new[]
|
||||||
|
@ -277,7 +277,7 @@ namespace Emby.Naming.Common
|
||||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||||
// so we make sure this one gets tested first.
|
// so we make sure this one gets tested first.
|
||||||
// "Foo Bar 889"
|
// "Foo Bar 889"
|
||||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
|
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
@ -300,32 +300,32 @@ namespace Emby.Naming.Common
|
||||||
// *** End Kodi Standard Naming
|
// *** End Kodi Standard Naming
|
||||||
|
|
||||||
// [bar] Foo - 1 [baz]
|
// [bar] Foo - 1 [baz]
|
||||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
|
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// "01.avi"
|
// "01.avi"
|
||||||
new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
|
new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
|
@ -335,34 +335,34 @@ namespace Emby.Naming.Common
|
||||||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||||
|
|
||||||
// "01 - blah.avi", "01-blah.avi"
|
// "01 - blah.avi", "01-blah.avi"
|
||||||
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// "01.blah.avi"
|
// "01.blah.avi"
|
||||||
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$")
|
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
|
// "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
|
||||||
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
|
new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// "01 episode title.avi"
|
// "01 episode title.avi"
|
||||||
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$")
|
new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
// "Episode 16", "Episode 16 - Title"
|
// "Episode 16", "Episode 16 - Title"
|
||||||
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
|
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
|
@ -625,17 +625,17 @@ namespace Emby.Naming.Common
|
||||||
AudioBookPartsExpressions = new[]
|
AudioBookPartsExpressions = new[]
|
||||||
{
|
{
|
||||||
// Detect specified chapters, like CH 01
|
// Detect specified chapters, like CH 01
|
||||||
@"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
|
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
|
||||||
// Detect specified parts, like Part 02
|
// Detect specified parts, like Part 02
|
||||||
@"p(?:ar)?t[\s_-]?(?<part>\d+)",
|
@"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
|
||||||
// Chapter is often beginning of filename
|
// Chapter is often beginning of filename
|
||||||
@"^(?<chapter>\d+)",
|
"^(?<chapter>[0-9]+)",
|
||||||
// Part if often ending of filename
|
// Part if often ending of filename
|
||||||
@"(?<part>\d+)$",
|
"(?<part>[0-9]+)$",
|
||||||
// Sometimes named as 0001_005 (chapter_part)
|
// Sometimes named as 0001_005 (chapter_part)
|
||||||
@"(?<chapter>\d+)_(?<part>\d+)",
|
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
|
||||||
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
||||||
@"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
|
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
|
||||||
};
|
};
|
||||||
|
|
||||||
var extensions = VideoFileExtensions.ToList();
|
var extensions = VideoFileExtensions.ToList();
|
||||||
|
@ -675,16 +675,16 @@ namespace Emby.Naming.Common
|
||||||
|
|
||||||
MultipleEpisodeExpressions = new string[]
|
MultipleEpisodeExpressions = new string[]
|
||||||
{
|
{
|
||||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
|
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
|
||||||
}.Select(i => new EpisodeExpression(i)
|
}.Select(i => new EpisodeExpression(i)
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Naming.TV
|
||||||
|
|
||||||
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var testFilename = filename.Substring(1);
|
var testFilename = filename.AsSpan().Slice(1);
|
||||||
|
|
||||||
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||||
{
|
{
|
||||||
|
|
|
@ -191,7 +191,7 @@ namespace Emby.Server.Implementations
|
||||||
/// Gets or sets the application paths.
|
/// Gets or sets the application paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The application paths.</value>
|
/// <value>The application paths.</value>
|
||||||
protected ServerApplicationPaths ApplicationPaths { get; set; }
|
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets all concrete types.
|
/// Gets or sets all concrete types.
|
||||||
|
@ -235,7 +235,7 @@ namespace Emby.Server.Implementations
|
||||||
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
|
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationHost(
|
public ApplicationHost(
|
||||||
ServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IStartupOptions options,
|
IStartupOptions options,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
|
@ -553,8 +553,6 @@ namespace Emby.Server.Implementations
|
||||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||||
|
@ -651,7 +649,6 @@ namespace Emby.Server.Implementations
|
||||||
_httpServer = Resolve<IHttpServer>();
|
_httpServer = Resolve<IHttpServer>();
|
||||||
_httpClient = Resolve<IHttpClient>();
|
_httpClient = Resolve<IHttpClient>();
|
||||||
|
|
||||||
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
|
||||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||||
|
|
||||||
SetStaticProperties();
|
SetStaticProperties();
|
||||||
|
@ -796,7 +793,6 @@ namespace Emby.Server.Implementations
|
||||||
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
||||||
|
|
||||||
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||||
Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
|
||||||
|
|
||||||
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
|
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -7,6 +6,7 @@ using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
|
@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
|
||||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
|
||||||
|
|
||||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
/// <param name="userDataManager">The user data manager.</param>
|
/// <param name="userDataManager">The user data manager.</param>
|
||||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||||
/// <param name="providerManager">The provider manager.</param>
|
/// <param name="providerManager">The provider manager.</param>
|
||||||
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
public ChannelManager(
|
public ChannelManager(
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IDtoService dtoService,
|
IDtoService dtoService,
|
||||||
|
@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IProviderManager providerManager)
|
IProviderManager providerManager,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_dtoService = dtoService;
|
_dtoService = dtoService;
|
||||||
|
@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
|
_memoryCache = memoryCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IChannel[] Channels { get; private set; }
|
internal IChannel[] Channels { get; private set; }
|
||||||
|
@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
|
||||||
|
|
||||||
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
|
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
|
if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
|
return cachedInfo;
|
||||||
{
|
|
||||||
return cachedInfo.Item2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
|
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
var list = mediaInfo.ToList();
|
var list = mediaInfo.ToList();
|
||||||
|
_memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
|
||||||
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
|
|
||||||
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,225 +0,0 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Common.Json;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class SQLiteDisplayPreferencesRepository.
|
|
||||||
/// </summary>
|
|
||||||
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
|
|
||||||
{
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
|
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
|
||||||
|
|
||||||
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
|
|
||||||
: base(logger)
|
|
||||||
{
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.GetOptions();
|
|
||||||
|
|
||||||
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the repository.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name.</value>
|
|
||||||
public string Name => "SQLite";
|
|
||||||
|
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InitializeInternal();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
|
|
||||||
|
|
||||||
_fileSystem.DeleteFile(DbFilePath);
|
|
||||||
|
|
||||||
InitializeInternal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the connection to the database.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private void InitializeInternal()
|
|
||||||
{
|
|
||||||
string[] queries =
|
|
||||||
{
|
|
||||||
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
|
|
||||||
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
|
|
||||||
};
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
connection.RunQueries(queries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save the display preferences associated with an item in the repo.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="displayPreferences">The display preferences.</param>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">item</exception>
|
|
||||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (displayPreferences == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(displayPreferences));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
connection.RunInTransaction(
|
|
||||||
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
|
|
||||||
TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
|
|
||||||
{
|
|
||||||
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
|
|
||||||
|
|
||||||
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
|
|
||||||
statement.TryBind("@userId", userId.ToByteArray());
|
|
||||||
statement.TryBind("@client", client);
|
|
||||||
statement.TryBind("@data", serialized);
|
|
||||||
|
|
||||||
statement.MoveNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save all display preferences associated with a user in the repo.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="displayPreferences">The display preferences.</param>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">item</exception>
|
|
||||||
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (displayPreferences == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(displayPreferences));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
connection.RunInTransaction(
|
|
||||||
db =>
|
|
||||||
{
|
|
||||||
foreach (var displayPreference in displayPreferences)
|
|
||||||
{
|
|
||||||
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the display preferences.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="displayPreferencesId">The display preferences id.</param>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <returns>Task{DisplayPreferences}.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">item</exception>
|
|
||||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(displayPreferencesId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(displayPreferencesId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var guidId = displayPreferencesId.GetMD5();
|
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
|
||||||
{
|
|
||||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@id", guidId.ToByteArray());
|
|
||||||
statement.TryBind("@userId", userId.ToByteArray());
|
|
||||||
statement.TryBind("@client", client);
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
return Get(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DisplayPreferences
|
|
||||||
{
|
|
||||||
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all display preferences for the given user.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <returns>Task{DisplayPreferences}.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">item</exception>
|
|
||||||
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
|
|
||||||
{
|
|
||||||
var list = new List<DisplayPreferences>();
|
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
|
||||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@userId", userId.ToByteArray());
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
list.Add(Get(row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
|
|
||||||
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
|
|
||||||
|
|
||||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
|
|
||||||
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
|
|
||||||
|
|
||||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
|
|
||||||
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@ using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Emby.Server.Implementations.Playlists;
|
using Emby.Server.Implementations.Playlists;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
|
||||||
"OwnerId"
|
"OwnerId"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
|
||||||
|
|
||||||
private static readonly string[] _mediaStreamSaveColumns =
|
private static readonly string[] _mediaStreamSaveColumns =
|
||||||
{
|
{
|
||||||
"ItemId",
|
"ItemId",
|
||||||
|
@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
|
||||||
"ColorTransfer"
|
"ColorTransfer"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||||
|
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
|
||||||
|
|
||||||
|
private static readonly string _mediaStreamSaveColumnsSelectQuery =
|
||||||
|
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
|
||||||
|
|
||||||
private static readonly string[] _mediaAttachmentSaveColumns =
|
private static readonly string[] _mediaAttachmentSaveColumns =
|
||||||
{
|
{
|
||||||
"ItemId",
|
"ItemId",
|
||||||
|
@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
|
||||||
"MIMEType"
|
"MIMEType"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
|
||||||
|
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
|
||||||
|
|
||||||
private static readonly string _mediaAttachmentInsertPrefix;
|
private static readonly string _mediaAttachmentInsertPrefix;
|
||||||
|
|
||||||
private static string GetSaveItemCommandText()
|
private const string SaveItemCommandText =
|
||||||
{
|
@"replace into TypedBaseItems
|
||||||
var saveColumns = new[]
|
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||||
{
|
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||||
"guid",
|
|
||||||
"type",
|
|
||||||
"data",
|
|
||||||
"Path",
|
|
||||||
"StartDate",
|
|
||||||
"EndDate",
|
|
||||||
"ChannelId",
|
|
||||||
"IsMovie",
|
|
||||||
"IsSeries",
|
|
||||||
"EpisodeTitle",
|
|
||||||
"IsRepeat",
|
|
||||||
"CommunityRating",
|
|
||||||
"CustomRating",
|
|
||||||
"IndexNumber",
|
|
||||||
"IsLocked",
|
|
||||||
"Name",
|
|
||||||
"OfficialRating",
|
|
||||||
"MediaType",
|
|
||||||
"Overview",
|
|
||||||
"ParentIndexNumber",
|
|
||||||
"PremiereDate",
|
|
||||||
"ProductionYear",
|
|
||||||
"ParentId",
|
|
||||||
"Genres",
|
|
||||||
"InheritedParentalRatingValue",
|
|
||||||
"SortName",
|
|
||||||
"ForcedSortName",
|
|
||||||
"RunTimeTicks",
|
|
||||||
"Size",
|
|
||||||
"DateCreated",
|
|
||||||
"DateModified",
|
|
||||||
"PreferredMetadataLanguage",
|
|
||||||
"PreferredMetadataCountryCode",
|
|
||||||
"Width",
|
|
||||||
"Height",
|
|
||||||
"DateLastRefreshed",
|
|
||||||
"DateLastSaved",
|
|
||||||
"IsInMixedFolder",
|
|
||||||
"LockedFields",
|
|
||||||
"Studios",
|
|
||||||
"Audio",
|
|
||||||
"ExternalServiceId",
|
|
||||||
"Tags",
|
|
||||||
"IsFolder",
|
|
||||||
"UnratedType",
|
|
||||||
"TopParentId",
|
|
||||||
"TrailerTypes",
|
|
||||||
"CriticRating",
|
|
||||||
"CleanName",
|
|
||||||
"PresentationUniqueKey",
|
|
||||||
"OriginalTitle",
|
|
||||||
"PrimaryVersionId",
|
|
||||||
"DateLastMediaAdded",
|
|
||||||
"Album",
|
|
||||||
"IsVirtualItem",
|
|
||||||
"SeriesName",
|
|
||||||
"UserDataKey",
|
|
||||||
"SeasonName",
|
|
||||||
"SeasonId",
|
|
||||||
"SeriesId",
|
|
||||||
"ExternalSeriesId",
|
|
||||||
"Tagline",
|
|
||||||
"ProviderIds",
|
|
||||||
"Images",
|
|
||||||
"ProductionLocations",
|
|
||||||
"ExtraIds",
|
|
||||||
"TotalBitrate",
|
|
||||||
"ExtraType",
|
|
||||||
"Artists",
|
|
||||||
"AlbumArtists",
|
|
||||||
"ExternalId",
|
|
||||||
"SeriesPresentationUniqueKey",
|
|
||||||
"ShowId",
|
|
||||||
"OwnerId"
|
|
||||||
};
|
|
||||||
|
|
||||||
var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
|
|
||||||
|
|
||||||
for (var i = 0; i < saveColumns.Length; i++)
|
|
||||||
{
|
|
||||||
if (i != 0)
|
|
||||||
{
|
|
||||||
saveItemCommandCommandText += ",";
|
|
||||||
}
|
|
||||||
|
|
||||||
saveItemCommandCommandText += "@" + saveColumns[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return saveItemCommandCommandText + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a standard item in the repo.
|
/// Save a standard item in the repo.
|
||||||
|
@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
var statements = PrepareAll(db, new string[]
|
var statements = PrepareAll(db, new string[]
|
||||||
{
|
{
|
||||||
GetSaveItemCommandText(),
|
SaveItemCommandText,
|
||||||
"delete from AncestorIds where ItemId=@ItemId"
|
"delete from AncestorIds where ItemId=@ItemId"
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
@ -1110,7 +1032,8 @@ namespace Emby.Server.Implementations.Data
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
str.Append(ToValueString(i) + "|");
|
str.Append(ToValueString(i))
|
||||||
|
.Append('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
str.Length -= 1; // Remove last |
|
str.Length -= 1; // Remove last |
|
||||||
|
@ -1225,7 +1148,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection(true))
|
||||||
{
|
{
|
||||||
using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
|
using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
|
||||||
{
|
{
|
||||||
statement.TryBind("@guid", id);
|
statement.TryBind("@guid", id);
|
||||||
|
|
||||||
|
@ -2471,7 +2394,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
var item = query.SimilarTo;
|
var item = query.SimilarTo;
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.Append("(");
|
builder.Append('(');
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(item.OfficialRating))
|
if (string.IsNullOrEmpty(item.OfficialRating))
|
||||||
{
|
{
|
||||||
|
@ -2509,7 +2432,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
if (!string.IsNullOrEmpty(query.SearchTerm))
|
if (!string.IsNullOrEmpty(query.SearchTerm))
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.Append("(");
|
builder.Append('(');
|
||||||
|
|
||||||
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
|
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
|
||||||
|
|
||||||
|
@ -2775,82 +2698,82 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
private string FixUnicodeChars(string buffer)
|
private string FixUnicodeChars(string buffer)
|
||||||
{
|
{
|
||||||
if (buffer.IndexOf('\u2013') > -1)
|
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2014') > -1)
|
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2015') > -1)
|
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2017') > -1)
|
if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2017', '_'); // double low line
|
buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2018') > -1)
|
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2019') > -1)
|
if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201a') > -1)
|
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201b') > -1)
|
if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201c') > -1)
|
if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201d') > -1)
|
if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201e') > -1)
|
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2026') > -1)
|
if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2032') > -1)
|
if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2032', '\''); // prime
|
buffer = buffer.Replace('\u2032', '\''); // prime
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2033') > -1)
|
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u0060') > -1)
|
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u00B4') > -1)
|
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||||
}
|
}
|
||||||
|
@ -2999,7 +2922,6 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
connection.RunInTransaction(db =>
|
connection.RunInTransaction(db =>
|
||||||
{
|
{
|
||||||
|
|
||||||
var statements = PrepareAll(db, statementTexts).ToList();
|
var statements = PrepareAll(db, statementTexts).ToList();
|
||||||
|
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
|
@ -4669,8 +4591,12 @@ namespace Emby.Server.Implementations.Data
|
||||||
|
|
||||||
if (query.BlockUnratedItems.Length > 1)
|
if (query.BlockUnratedItems.Length > 1)
|
||||||
{
|
{
|
||||||
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||||
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
|
whereClauses.Add(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||||
|
inClause));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.ExcludeInheritedTags.Length > 0)
|
if (query.ExcludeInheritedTags.Length > 0)
|
||||||
|
@ -4679,7 +4605,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
if (statement == null)
|
if (statement == null)
|
||||||
{
|
{
|
||||||
int index = 0;
|
int index = 0;
|
||||||
string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
|
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -5238,7 +5164,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||||
{
|
{
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
insertText.Append(",");
|
insertText.Append(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
|
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
|
||||||
|
@ -5890,10 +5816,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||||
throw new ArgumentNullException(nameof(query));
|
throw new ArgumentNullException(nameof(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdText = "select "
|
var cmdText = _mediaStreamSaveColumnsSelectQuery;
|
||||||
+ string.Join(",", _mediaStreamSaveColumns)
|
|
||||||
+ " from mediastreams where"
|
|
||||||
+ " ItemId=@ItemId";
|
|
||||||
|
|
||||||
if (query.Type.HasValue)
|
if (query.Type.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -5972,15 +5895,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||||
|
|
||||||
while (startIndex < streams.Count)
|
while (startIndex < streams.Count)
|
||||||
{
|
{
|
||||||
var insertText = new StringBuilder("insert into mediastreams (");
|
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
|
||||||
foreach (var column in _mediaStreamSaveColumns)
|
|
||||||
{
|
|
||||||
insertText.Append(column).Append(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove last comma
|
|
||||||
insertText.Length--;
|
|
||||||
insertText.Append(") values ");
|
|
||||||
|
|
||||||
var endIndex = Math.Min(streams.Count, startIndex + Limit);
|
var endIndex = Math.Min(streams.Count, startIndex + Limit);
|
||||||
|
|
||||||
|
@ -6247,10 +6162,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||||
throw new ArgumentNullException(nameof(query));
|
throw new ArgumentNullException(nameof(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdText = "select "
|
var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
|
||||||
+ string.Join(",", _mediaAttachmentSaveColumns)
|
|
||||||
+ " from mediaattachments where"
|
|
||||||
+ " ItemId=@ItemId";
|
|
||||||
|
|
||||||
if (query.Index.HasValue)
|
if (query.Index.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -6331,7 +6243,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||||
|
|
||||||
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
|
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
|
||||||
{
|
{
|
||||||
insertText.Append("@" + column + index + ",");
|
insertText.Append('@')
|
||||||
|
.Append(column)
|
||||||
|
.Append(index)
|
||||||
|
.Append(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
insertText.Length -= 1;
|
insertText.Length -= 1;
|
||||||
|
|
|
@ -5,8 +5,8 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
|
@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Devices
|
namespace Emby.Server.Implementations.Devices
|
||||||
{
|
{
|
||||||
public class DeviceManager : IDeviceManager
|
public class DeviceManager : IDeviceManager
|
||||||
{
|
{
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly IJsonSerializer _json;
|
private readonly IJsonSerializer _json;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IAuthenticationRepository _authRepo;
|
private readonly IAuthenticationRepository _authRepo;
|
||||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
|
||||||
private readonly object _capabilitiesSyncLock = new object();
|
private readonly object _capabilitiesSyncLock = new object();
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||||
|
@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
|
||||||
IAuthenticationRepository authRepo,
|
IAuthenticationRepository authRepo,
|
||||||
IJsonSerializer json,
|
IJsonSerializer json,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IServerConfigurationManager config)
|
IServerConfigurationManager config,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
_json = json;
|
_json = json;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_memoryCache = memoryCache;
|
||||||
_authRepo = authRepo;
|
_authRepo = authRepo;
|
||||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||||
|
@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
|
||||||
|
|
||||||
lock (_capabilitiesSyncLock)
|
lock (_capabilitiesSyncLock)
|
||||||
{
|
{
|
||||||
_capabilitiesCache[deviceId] = capabilities;
|
_memoryCache.CreateEntry(deviceId).SetValue(capabilities);
|
||||||
|
|
||||||
_json.SerializeToFile(capabilities, path);
|
_json.SerializeToFile(capabilities, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
|
||||||
|
|
||||||
public ClientCapabilities GetCapabilities(string id)
|
public ClientCapabilities GetCapabilities(string id)
|
||||||
{
|
{
|
||||||
|
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
lock (_capabilitiesSyncLock)
|
lock (_capabilitiesSyncLock)
|
||||||
{
|
{
|
||||||
if (_capabilitiesCache.TryGetValue(id, out var result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
||||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
|
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
<PackageReference Include="Mono.Nat" Version="2.0.2" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.25.1" />
|
<PackageReference Include="sharpcompress" Version="0.26.0" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
|
|
@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
private readonly IStreamHelper _streamHelper;
|
private readonly IStreamHelper _streamHelper;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _options.
|
/// The _options.
|
||||||
|
@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
_streamHelper = streamHelper;
|
_streamHelper = streamHelper;
|
||||||
_fileSystem = fileSystem;
|
|
||||||
|
|
||||||
Path = path;
|
Path = path;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
|
@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
httpRes.StatusCode = 200;
|
httpRes.StatusCode = 200;
|
||||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||||
{
|
{
|
||||||
httpRes.Headers.Add(key, value);
|
httpRes.Headers.Add(key, value);
|
||||||
}
|
}
|
||||||
|
@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
var handler = GetServiceHandler(httpReq);
|
var handler = GetServiceHandler(httpReq);
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
|
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer.Security
|
namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
{
|
{
|
||||||
public class AuthService : IAuthService
|
public class AuthService : IAuthService
|
||||||
{
|
{
|
||||||
private readonly ILogger<AuthService> _logger;
|
|
||||||
private readonly IAuthorizationContext _authorizationContext;
|
private readonly IAuthorizationContext _authorizationContext;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
|
|
||||||
public AuthService(
|
public AuthService(
|
||||||
ILogger<AuthService> logger,
|
|
||||||
IAuthorizationContext authorizationContext,
|
IAuthorizationContext authorizationContext,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
INetworkManager networkManager)
|
INetworkManager networkManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_authorizationContext = authorizationContext;
|
_authorizationContext = authorizationContext;
|
||||||
_config = config;
|
_config = config;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
|
|
@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
private readonly List<string> _affectedPaths = new List<string>();
|
private readonly List<string> _affectedPaths = new List<string>();
|
||||||
private readonly object _timerLock = new object();
|
private readonly object _timerLock = new object();
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||||
{
|
{
|
||||||
|
@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _disposed;
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
DisposeTimer();
|
DisposeTimer();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
|
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
|
||||||
{
|
{
|
||||||
/// <summary>
|
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
|
||||||
/// The library manager.
|
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||||
/// </summary>
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
|
|
||||||
public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Emby.Server.Implementations.Images;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Emby.Server.Implementations.Images;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
// Don't resolve these into audio files
|
// Don't resolve these into audio files
|
||||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
|
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
||||||
&& _libraryManager.IsAudioFile(filename))
|
&& _libraryManager.IsAudioFile(filename))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
public class ExclusiveLiveStream : ILiveStream
|
public class ExclusiveLiveStream : ILiveStream
|
||||||
{
|
{
|
||||||
|
private readonly Func<Task> _closeFn;
|
||||||
|
|
||||||
|
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
|
||||||
|
{
|
||||||
|
MediaSource = mediaSource;
|
||||||
|
EnableStreamSharing = false;
|
||||||
|
_closeFn = closeFn;
|
||||||
|
ConsumerCount = 1;
|
||||||
|
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
public int ConsumerCount { get; set; }
|
public int ConsumerCount { get; set; }
|
||||||
|
|
||||||
public string OriginalStreamId { get; set; }
|
public string OriginalStreamId { get; set; }
|
||||||
|
@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
public MediaSourceInfo MediaSource { get; set; }
|
public MediaSourceInfo MediaSource { get; set; }
|
||||||
|
|
||||||
public string UniqueId { get; private set; }
|
public string UniqueId { get; }
|
||||||
|
|
||||||
private Func<Task> _closeFn;
|
|
||||||
|
|
||||||
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
|
|
||||||
{
|
|
||||||
MediaSource = mediaSource;
|
|
||||||
EnableStreamSharing = false;
|
|
||||||
_closeFn = closeFn;
|
|
||||||
ConsumerCount = 1;
|
|
||||||
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Close()
|
public Task Close()
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
"**/small.jpg",
|
"**/small.jpg",
|
||||||
"**/albumart.jpg",
|
"**/albumart.jpg",
|
||||||
"**/*sample*",
|
|
||||||
|
// We have neither non-greedy matching or character group repetitions, working around that here.
|
||||||
|
// https://github.com/dazinator/DotNet.Glob#patterns
|
||||||
|
// .*/sample\..{1,5}
|
||||||
|
"**/sample.?",
|
||||||
|
"**/sample.??",
|
||||||
|
"**/sample.???", // Matches sample.mkv
|
||||||
|
"**/sample.????", // Matches sample.webm
|
||||||
|
"**/sample.?????",
|
||||||
|
"**/*.sample.?",
|
||||||
|
"**/*.sample.??",
|
||||||
|
"**/*.sample.???",
|
||||||
|
"**/*.sample.????",
|
||||||
|
"**/*.sample.?????",
|
||||||
|
"**/sample/*",
|
||||||
|
|
||||||
// Directories
|
// Directories
|
||||||
"**/metadata/**",
|
"**/metadata/**",
|
||||||
|
@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
|
||||||
"**/.grab/**",
|
"**/.grab/**",
|
||||||
"**/.grab",
|
"**/.grab",
|
||||||
|
|
||||||
// Unix hidden files and directories
|
// Unix hidden files
|
||||||
"**/.*/**",
|
|
||||||
"**/.*",
|
"**/.*",
|
||||||
|
|
||||||
|
// Mac - if you ever remove the above.
|
||||||
|
// "**/._*",
|
||||||
|
// "**/.DS_Store",
|
||||||
|
|
||||||
// thumbs.db
|
// thumbs.db
|
||||||
"**/thumbs.db",
|
"**/thumbs.db",
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using MediaBrowser.Providers.MediaInfo;
|
using MediaBrowser.Providers.MediaInfo;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||||
using Person = MediaBrowser.Controller.Entities.Person;
|
using Person = MediaBrowser.Controller.Entities.Person;
|
||||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
|
||||||
using VideoResolver = Emby.Naming.Video.VideoResolver;
|
using VideoResolver = Emby.Naming.Video.VideoResolver;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
|
@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LibraryManager : ILibraryManager
|
public class LibraryManager : ILibraryManager
|
||||||
{
|
{
|
||||||
|
private const string ShortcutFileExtension = ".mblink";
|
||||||
|
|
||||||
private readonly ILogger<LibraryManager> _logger;
|
private readonly ILogger<LibraryManager> _logger;
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly ITaskManager _taskManager;
|
private readonly ITaskManager _taskManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IUserDataManager _userDataRepository;
|
private readonly IUserDataManager _userDataRepository;
|
||||||
|
@ -72,12 +74,118 @@ namespace Emby.Server.Implementations.Library
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IItemRepository _itemRepository;
|
private readonly IItemRepository _itemRepository;
|
||||||
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
|
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _root folder sync lock.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _rootFolderSyncLock = new object();
|
||||||
|
private readonly object _userRootFolderSyncLock = new object();
|
||||||
|
|
||||||
|
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||||
|
|
||||||
private NamingOptions _namingOptions;
|
private NamingOptions _namingOptions;
|
||||||
private string[] _videoFileExtensions;
|
private string[] _videoFileExtensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _root folder.
|
||||||
|
/// </summary>
|
||||||
|
private volatile AggregateFolder _rootFolder;
|
||||||
|
private volatile UserRootFolder _userRootFolder;
|
||||||
|
|
||||||
|
private bool _wizardCompleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appHost">The application host.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="taskManager">The task manager.</param>
|
||||||
|
/// <param name="userManager">The user manager.</param>
|
||||||
|
/// <param name="configurationManager">The configuration manager.</param>
|
||||||
|
/// <param name="userDataRepository">The user data repository.</param>
|
||||||
|
/// <param name="libraryMonitorFactory">The library monitor.</param>
|
||||||
|
/// <param name="fileSystem">The file system.</param>
|
||||||
|
/// <param name="providerManagerFactory">The provider manager.</param>
|
||||||
|
/// <param name="userviewManagerFactory">The userview manager.</param>
|
||||||
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
|
/// <param name="itemRepository">The item repository.</param>
|
||||||
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
|
public LibraryManager(
|
||||||
|
IServerApplicationHost appHost,
|
||||||
|
ILogger<LibraryManager> logger,
|
||||||
|
ITaskManager taskManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IUserDataManager userDataRepository,
|
||||||
|
Lazy<ILibraryMonitor> libraryMonitorFactory,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
Lazy<IProviderManager> providerManagerFactory,
|
||||||
|
Lazy<IUserViewManager> userviewManagerFactory,
|
||||||
|
IMediaEncoder mediaEncoder,
|
||||||
|
IItemRepository itemRepository,
|
||||||
|
IImageProcessor imageProcessor,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
|
{
|
||||||
|
_appHost = appHost;
|
||||||
|
_logger = logger;
|
||||||
|
_taskManager = taskManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
|
_userDataRepository = userDataRepository;
|
||||||
|
_libraryMonitorFactory = libraryMonitorFactory;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_providerManagerFactory = providerManagerFactory;
|
||||||
|
_userviewManagerFactory = userviewManagerFactory;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
_itemRepository = itemRepository;
|
||||||
|
_imageProcessor = imageProcessor;
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
|
||||||
|
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||||
|
|
||||||
|
RecordConfigurationValues(configurationManager.Configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [item added].
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<ItemChangeEventArgs> ItemAdded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [item updated].
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when [item removed].
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the root folder.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The root folder.</value>
|
||||||
|
public AggregateFolder RootFolder
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_rootFolder == null)
|
||||||
|
{
|
||||||
|
lock (_rootFolderSyncLock)
|
||||||
|
{
|
||||||
|
if (_rootFolder == null)
|
||||||
|
{
|
||||||
|
_rootFolder = CreateRootFolder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _rootFolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
|
private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
|
||||||
|
|
||||||
private IProviderManager ProviderManager => _providerManagerFactory.Value;
|
private IProviderManager ProviderManager => _providerManagerFactory.Value;
|
||||||
|
@ -116,75 +224,8 @@ namespace Emby.Server.Implementations.Library
|
||||||
/// <value>The comparers.</value>
|
/// <value>The comparers.</value>
|
||||||
private IBaseItemComparer[] Comparers { get; set; }
|
private IBaseItemComparer[] Comparers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [item added].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ItemChangeEventArgs> ItemAdded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [item updated].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ItemChangeEventArgs> ItemUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [item removed].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
|
|
||||||
|
|
||||||
public bool IsScanRunning { get; private set; }
|
public bool IsScanRunning { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="appHost">The application host.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="taskManager">The task manager.</param>
|
|
||||||
/// <param name="userManager">The user manager.</param>
|
|
||||||
/// <param name="configurationManager">The configuration manager.</param>
|
|
||||||
/// <param name="userDataRepository">The user data repository.</param>
|
|
||||||
/// <param name="libraryMonitorFactory">The library monitor.</param>
|
|
||||||
/// <param name="fileSystem">The file system.</param>
|
|
||||||
/// <param name="providerManagerFactory">The provider manager.</param>
|
|
||||||
/// <param name="userviewManagerFactory">The userview manager.</param>
|
|
||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
|
||||||
/// <param name="itemRepository">The item repository.</param>
|
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
|
||||||
public LibraryManager(
|
|
||||||
IServerApplicationHost appHost,
|
|
||||||
ILogger<LibraryManager> logger,
|
|
||||||
ITaskManager taskManager,
|
|
||||||
IUserManager userManager,
|
|
||||||
IServerConfigurationManager configurationManager,
|
|
||||||
IUserDataManager userDataRepository,
|
|
||||||
Lazy<ILibraryMonitor> libraryMonitorFactory,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
Lazy<IProviderManager> providerManagerFactory,
|
|
||||||
Lazy<IUserViewManager> userviewManagerFactory,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IItemRepository itemRepository,
|
|
||||||
IImageProcessor imageProcessor)
|
|
||||||
{
|
|
||||||
_appHost = appHost;
|
|
||||||
_logger = logger;
|
|
||||||
_taskManager = taskManager;
|
|
||||||
_userManager = userManager;
|
|
||||||
_configurationManager = configurationManager;
|
|
||||||
_userDataRepository = userDataRepository;
|
|
||||||
_libraryMonitorFactory = libraryMonitorFactory;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_providerManagerFactory = providerManagerFactory;
|
|
||||||
_userviewManagerFactory = userviewManagerFactory;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_itemRepository = itemRepository;
|
|
||||||
_imageProcessor = imageProcessor;
|
|
||||||
|
|
||||||
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
|
|
||||||
|
|
||||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
|
||||||
|
|
||||||
RecordConfigurationValues(configurationManager.Configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the parts.
|
/// Adds the parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -208,41 +249,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
PostscanTasks = postscanTasks.ToArray();
|
PostscanTasks = postscanTasks.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _root folder.
|
|
||||||
/// </summary>
|
|
||||||
private volatile AggregateFolder _rootFolder;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _root folder sync lock.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _rootFolderSyncLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the root folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The root folder.</value>
|
|
||||||
public AggregateFolder RootFolder
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_rootFolder == null)
|
|
||||||
{
|
|
||||||
lock (_rootFolderSyncLock)
|
|
||||||
{
|
|
||||||
if (_rootFolder == null)
|
|
||||||
{
|
|
||||||
_rootFolder = CreateRootFolder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _rootFolder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _wizardCompleted;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records the configuration values.
|
/// Records the configuration values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
|
_memoryCache.CreateEntry(item.Id).SetValue(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||||
|
@ -441,7 +447,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
_itemRepository.DeleteItem(child.Id);
|
_itemRepository.DeleteItem(child.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
|
_memoryCache.Remove(item.Id);
|
||||||
|
|
||||||
ReportItemRemoved(item, parent);
|
ReportItemRemoved(item, parent);
|
||||||
}
|
}
|
||||||
|
@ -511,8 +517,8 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
// Try to normalize paths located underneath program-data in an attempt to make them more portable
|
// Try to normalize paths located underneath program-data in an attempt to make them more portable
|
||||||
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
|
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
|
||||||
.TrimStart(new[] { '/', '\\' })
|
.TrimStart('/', '\\')
|
||||||
.Replace("/", "\\");
|
.Replace('/', '\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
|
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
|
||||||
|
@ -775,14 +781,11 @@ namespace Emby.Server.Implementations.Library
|
||||||
return rootFolder;
|
return rootFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private volatile UserRootFolder _userRootFolder;
|
|
||||||
private readonly object _syncLock = new object();
|
|
||||||
|
|
||||||
public Folder GetUserRootFolder()
|
public Folder GetUserRootFolder()
|
||||||
{
|
{
|
||||||
if (_userRootFolder == null)
|
if (_userRootFolder == null)
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_userRootFolderSyncLock)
|
||||||
{
|
{
|
||||||
if (_userRootFolder == null)
|
if (_userRootFolder == null)
|
||||||
{
|
{
|
||||||
|
@ -1245,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
|
if (_memoryCache.TryGetValue(id, out BaseItem item))
|
||||||
{
|
{
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -1332,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
return new QueryResult<BaseItem>
|
return new QueryResult<BaseItem>
|
||||||
{
|
{
|
||||||
Items = _itemRepository.GetItemList(query).ToArray()
|
Items = _itemRepository.GetItemList(query)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1463,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
return _itemRepository.GetItems(query);
|
return _itemRepository.GetItems(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = _itemRepository.GetItemList(query);
|
|
||||||
|
|
||||||
return new QueryResult<BaseItem>
|
return new QueryResult<BaseItem>
|
||||||
{
|
{
|
||||||
Items = list
|
Items = _itemRepository.GetItemList(query)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1590,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||||
{
|
{
|
||||||
var tasks = IntroProviders
|
var tasks = IntroProviders
|
||||||
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
|
|
||||||
.Take(1)
|
.Take(1)
|
||||||
.Select(i => GetIntros(i, item, user));
|
.Select(i => GetIntros(i, item, user));
|
||||||
|
|
||||||
|
@ -1876,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||||
if (outdated.Length == 0)
|
// Skip image processing if current or live tv source
|
||||||
|
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
||||||
{
|
{
|
||||||
RegisterItem(item);
|
RegisterItem(item);
|
||||||
return;
|
return;
|
||||||
|
@ -1945,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the item.
|
/// Updates the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Don't iterate multiple times
|
foreach (var item in items)
|
||||||
var itemsList = items.ToList();
|
|
||||||
|
|
||||||
foreach (var item in itemsList)
|
|
||||||
{
|
{
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
{
|
{
|
||||||
|
@ -1962,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
|
||||||
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
|
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(itemsList, cancellationToken);
|
_itemRepository.SaveItems(items, cancellationToken);
|
||||||
|
|
||||||
if (ItemUpdated != null)
|
if (ItemUpdated != null)
|
||||||
{
|
{
|
||||||
foreach (var item in itemsList)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
// With the live tv guide this just creates too much noise
|
// With the live tv guide this just creates too much noise
|
||||||
if (item.SourceType != SourceType.Library)
|
if (item.SourceType != SourceType.Library)
|
||||||
|
@ -2189,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
|
||||||
|
|
||||||
public UserView GetNamedView(
|
public UserView GetNamedView(
|
||||||
User user,
|
User user,
|
||||||
string name,
|
string name,
|
||||||
|
@ -2488,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||||
|
|
||||||
var episodeInfo = episode.IsFileProtocol ?
|
var episodeInfo = episode.IsFileProtocol
|
||||||
resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
|
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
|
||||||
new Naming.TV.EpisodeInfo();
|
: new Naming.TV.EpisodeInfo();
|
||||||
|
|
||||||
if (episodeInfo == null)
|
|
||||||
{
|
|
||||||
episodeInfo = new Naming.TV.EpisodeInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -2503,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
|
||||||
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Read from metadata
|
// Read from metadata
|
||||||
var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
var mediaInfo = _mediaEncoder.GetMediaInfo(
|
||||||
{
|
new MediaInfoRequest
|
||||||
MediaSource = episode.GetMediaSources(false)[0],
|
{
|
||||||
MediaType = DlnaProfileType.Video
|
MediaSource = episode.GetMediaSources(false)[0],
|
||||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
MediaType = DlnaProfileType.Video
|
||||||
|
},
|
||||||
|
CancellationToken.None).GetAwaiter().GetResult();
|
||||||
if (mediaInfo.ParentIndexNumber > 0)
|
if (mediaInfo.ParentIndexNumber > 0)
|
||||||
{
|
{
|
||||||
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
|
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
|
||||||
|
@ -2665,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
var videos = videoListResolver.Resolve(fileSystemChildren);
|
||||||
|
|
||||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
|
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (currentVideo != null)
|
if (currentVideo != null)
|
||||||
{
|
{
|
||||||
|
@ -2682,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
.Select(video =>
|
.Select(video =>
|
||||||
{
|
{
|
||||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||||
var dbItem = GetItemById(video.Id) as Trailer;
|
if (GetItemById(video.Id) is Trailer dbItem)
|
||||||
|
|
||||||
if (dbItem != null)
|
|
||||||
{
|
{
|
||||||
video = dbItem;
|
video = dbItem;
|
||||||
}
|
}
|
||||||
|
@ -3011,8 +3002,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string ShortcutFileExtension = ".mblink";
|
|
||||||
|
|
||||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||||
{
|
{
|
||||||
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
||||||
|
@ -3206,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (!Directory.Exists(virtualFolderPath))
|
if (!Directory.Exists(virtualFolderPath))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
|
throw new FileNotFoundException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
|
||||||
}
|
}
|
||||||
|
|
||||||
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
||||||
|
|
|
@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IJsonSerializer _json;
|
||||||
private IJsonSerializer _json;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private IApplicationPaths _appPaths;
|
|
||||||
|
|
||||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
|
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
|
||||||
{
|
{
|
||||||
|
@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
mediaSource.AnalyzeDurationMs = 3000;
|
mediaSource.AnalyzeDurationMs = 3000;
|
||||||
|
|
||||||
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||||
{
|
new MediaInfoRequest
|
||||||
MediaSource = mediaSource,
|
{
|
||||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
MediaSource = mediaSource,
|
||||||
ExtractChapters = false
|
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||||
|
ExtractChapters = false
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
},
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (cacheFilePath != null)
|
if (cacheFilePath != null)
|
||||||
{
|
{
|
||||||
|
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
mediaSource.RunTimeTicks = null;
|
mediaSource.RunTimeTicks = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
|
var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
|
||||||
|
|
||||||
if (audioStream == null || audioStream.Index == -1)
|
if (audioStream == null || audioStream.Index == -1)
|
||||||
{
|
{
|
||||||
|
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
|
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
||||||
if (videoStream != null)
|
if (videoStream != null)
|
||||||
{
|
{
|
||||||
if (!videoStream.BitRate.HasValue)
|
if (!videoStream.BitRate.HasValue)
|
||||||
|
|
|
@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
public class MediaSourceManager : IMediaSourceManager, IDisposable
|
public class MediaSourceManager : IMediaSourceManager, IDisposable
|
||||||
{
|
{
|
||||||
|
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
||||||
|
private const char LiveStreamIdDelimeter = '_';
|
||||||
|
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
private IMediaSourceProvider[] _providers;
|
private IMediaSourceProvider[] _providers;
|
||||||
|
|
||||||
public MediaSourceManager(
|
public MediaSourceManager(
|
||||||
|
@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
||||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||||
|
|
||||||
|
@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
|
||||||
|
|
||||||
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (liveStreamInfo is IDirectStreamProvider)
|
if (liveStreamInfo is IDirectStreamProvider)
|
||||||
{
|
{
|
||||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
var info = await _mediaEncoder.GetMediaInfo(
|
||||||
{
|
new MediaInfoRequest
|
||||||
MediaSource = mediaSource,
|
{
|
||||||
ExtractChapters = false,
|
MediaSource = mediaSource,
|
||||||
MediaType = DlnaProfileType.Video
|
ExtractChapters = false,
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
MediaType = DlnaProfileType.Video
|
||||||
|
},
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
mediaSource.MediaStreams = info.MediaStreams;
|
mediaSource.MediaStreams = info.MediaStreams;
|
||||||
mediaSource.Container = info.Container;
|
mediaSource.Container = info.Container;
|
||||||
|
@ -855,24 +859,21 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
private (IMediaSourceProvider, string) GetProvider(string key)
|
||||||
private const char LiveStreamIdDelimeter = '_';
|
|
||||||
|
|
||||||
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("key");
|
throw new ArgumentException("Key can't be empty.", nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
||||||
|
|
||||||
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
|
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
|
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
||||||
var keyId = key.Substring(splitIndex + 1);
|
var keyId = key.Substring(splitIndex + 1);
|
||||||
|
|
||||||
return new Tuple<IMediaSourceProvider, string>(provider, keyId);
|
return (provider, keyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly object _disposeLock = new object();
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
/// Releases unmanaged and - optionally - managed resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
if (dispose)
|
if (dispose)
|
||||||
{
|
{
|
||||||
lock (_disposeLock)
|
foreach (var key in _openStreams.Keys.ToList())
|
||||||
{
|
{
|
||||||
foreach (var key in _openStreams.Keys.ToList())
|
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||||
{
|
|
||||||
var task = CloseLiveStream(key);
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_liveStreamSemaphore.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
// load forced subs if we have found no suitable full subtitles
|
// load forced subs if we have found no suitable full subtitles
|
||||||
stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
|
stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
|
@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SeasonResolver : FolderResolver<Season>
|
public class SeasonResolver : FolderResolver<Season>
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly ILogger<SeasonResolver> _logger;
|
private readonly ILogger<SeasonResolver> _logger;
|
||||||
|
@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="config">The config.</param>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
/// <param name="localization">The localization.</param>
|
/// <param name="localization">The localization.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
public SeasonResolver(
|
public SeasonResolver(
|
||||||
IServerConfigurationManager config,
|
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
ILogger<SeasonResolver> logger)
|
ILogger<SeasonResolver> logger)
|
||||||
{
|
{
|
||||||
_config = config;
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Extensions;
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Search;
|
using MediaBrowser.Model.Search;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
public class SearchEngine : ISearchEngine
|
public class SearchEngine : ISearchEngine
|
||||||
{
|
{
|
||||||
private readonly ILogger<SearchEngine> _logger;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
|
public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
|
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
|
||||||
{
|
{
|
||||||
User user = null;
|
User user = null;
|
||||||
|
if (query.UserId != Guid.Empty)
|
||||||
if (query.UserId.Equals(Guid.Empty))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
user = _userManager.GetUserById(query.UserId);
|
user = _userManager.GetUserById(query.UserId);
|
||||||
}
|
}
|
||||||
|
@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (query.StartIndex.HasValue)
|
if (query.StartIndex.HasValue)
|
||||||
{
|
{
|
||||||
results = results.Skip(query.StartIndex.Value).ToList();
|
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
if (query.Limit.HasValue)
|
||||||
{
|
{
|
||||||
results = results.Take(query.Limit.Value).ToList();
|
results = results.GetRange(0, query.Limit.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new QueryResult<SearchHintInfo>
|
return new QueryResult<SearchHintInfo>
|
||||||
{
|
{
|
||||||
TotalRecordCount = totalRecordCount,
|
TotalRecordCount = totalRecordCount,
|
||||||
|
|
||||||
Items = results.ToArray()
|
Items = results
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(searchTerm))
|
if (string.IsNullOrEmpty(searchTerm))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm));
|
throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
||||||
|
|
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Book = MediaBrowser.Controller.Entities.Book;
|
using Book = MediaBrowser.Controller.Entities.Book;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
|
@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
|
||||||
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
||||||
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private readonly ILogger<UserDataManager> _logger;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IUserDataRepository _repository;
|
private readonly IUserDataRepository _repository;
|
||||||
|
|
||||||
public UserDataManager(
|
public UserDataManager(
|
||||||
ILogger<UserDataManager> logger,
|
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IUserDataRepository repository)
|
IUserDataRepository repository)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
_config = config;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Server.Implementations.Library;
|
using Emby.Server.Implementations.Library;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
|
|
|
@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
|
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
|
||||||
ListingsProviderInfo info,
|
ListingsProviderInfo info,
|
||||||
List<string> programIds,
|
List<string> programIds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (programIds.Count == 0)
|
if (programIds.Count == 0)
|
||||||
{
|
{
|
||||||
|
@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
{
|
{
|
||||||
var imageId = i.Substring(0, 10);
|
var imageId = i.Substring(0, 10);
|
||||||
|
|
||||||
if (!imageIdString.Contains(imageId))
|
if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
imageIdString += "\"" + imageId + "\",";
|
imageIdString += "\"" + imageId + "\",";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Library;
|
using Emby.Server.Implementations.Library;
|
||||||
|
@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.LiveTv;
|
using MediaBrowser.Model.LiveTv;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
|
@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ITaskManager _taskManager;
|
private readonly ITaskManager _taskManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IChannelManager _channelManager;
|
private readonly IChannelManager _channelManager;
|
||||||
private readonly LiveTvDtoService _tvDtoService;
|
private readonly LiveTvDtoService _tvDtoService;
|
||||||
|
@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
ITaskManager taskManager,
|
ITaskManager taskManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IChannelManager channelManager,
|
IChannelManager channelManager,
|
||||||
LiveTvDtoService liveTvDtoService)
|
LiveTvDtoService liveTvDtoService)
|
||||||
|
@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_taskManager = taskManager;
|
_taskManager = taskManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_dtoService = dtoService;
|
_dtoService = dtoService;
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
|
@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
|
|
||||||
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
|
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
|
||||||
{
|
{
|
||||||
info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
|
info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
|
||||||
|
|
||||||
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
{
|
{
|
||||||
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
|
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
|
||||||
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
|
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
|
||||||
info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
|
info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
|
||||||
|
|
||||||
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
while (!sr.EndOfStream)
|
while (!sr.EndOfStream)
|
||||||
{
|
{
|
||||||
string line = StripXML(sr.ReadLine());
|
string line = StripXML(sr.ReadLine());
|
||||||
if (line.Contains("Channel"))
|
if (line.Contains("Channel", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
LiveTvTunerStatus status;
|
LiveTvTunerStatus status;
|
||||||
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||||
|
@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
private static string StripXML(string source)
|
private static string StripXML(string source)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(source))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
char[] buffer = new char[source.Length];
|
char[] buffer = new char[source.Length];
|
||||||
int bufferIndex = 0;
|
int bufferIndex = 0;
|
||||||
bool inside = false;
|
bool inside = false;
|
||||||
|
@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
for (int i = 0; i < model.TunerCount; ++i)
|
for (int i = 0; i < model.TunerCount; ++i)
|
||||||
{
|
{
|
||||||
var name = string.Format("Tuner {0}", i + 1);
|
var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
|
||||||
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
|
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
|
||||||
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
|
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
|
||||||
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
|
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HdHomerunManager : IDisposable
|
public sealed class HdHomerunManager : IDisposable
|
||||||
{
|
{
|
||||||
public const int HdHomeRunPort = 65001;
|
public const int HdHomeRunPort = 65001;
|
||||||
|
|
||||||
|
@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
StopStreaming(socket).GetAwaiter().GetResult();
|
StopStreaming(socket).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||||
|
@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeTuner = i;
|
_activeTuner = i;
|
||||||
var lockKeyString = string.Format("{0:d}", lockKeyValue);
|
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
|
||||||
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
||||||
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandList = commands.GetCommands();
|
foreach (var command in commands.GetCommands())
|
||||||
foreach (var command in commandList)
|
|
||||||
{
|
{
|
||||||
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
||||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
|
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
|
||||||
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
||||||
|
|
||||||
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
|
@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
||||||
{
|
{
|
||||||
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
|
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
|
||||||
|
|
||||||
string numberString = null;
|
string numberString = null;
|
||||||
string attributeValue;
|
string attributeValue;
|
||||||
double doubleValue;
|
|
||||||
|
|
||||||
if (attributes.TryGetValue("tvg-chno", out attributeValue))
|
if (attributes.TryGetValue("tvg-chno", out attributeValue))
|
||||||
{
|
{
|
||||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
|
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
numberString = attributeValue;
|
numberString = attributeValue;
|
||||||
}
|
}
|
||||||
|
@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
{
|
{
|
||||||
if (attributes.TryGetValue("tvg-id", out attributeValue))
|
if (attributes.TryGetValue("tvg-id", out attributeValue))
|
||||||
{
|
{
|
||||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
|
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
numberString = attributeValue;
|
numberString = attributeValue;
|
||||||
}
|
}
|
||||||
else if (attributes.TryGetValue("channel-id", out attributeValue))
|
else if (attributes.TryGetValue("channel-id", out attributeValue))
|
||||||
{
|
{
|
||||||
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
|
if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
numberString = attributeValue;
|
numberString = attributeValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String.IsNullOrWhiteSpace(numberString))
|
if (string.IsNullOrWhiteSpace(numberString))
|
||||||
{
|
{
|
||||||
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
|
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
|
||||||
// where 5 isnt ment to be the channel number
|
// where 5 isnt ment to be the channel number
|
||||||
// Check for channel number with the format from SatIp
|
// Check for channel number with the format from SatIp
|
||||||
// #EXTINF:0,84. VOX Schweiz
|
// #EXTINF:0,84. VOX Schweiz
|
||||||
// #EXTINF:0,84.0 - VOX Schweiz
|
// #EXTINF:0,84.0 - VOX Schweiz
|
||||||
if (!string.IsNullOrWhiteSpace(nameInExtInf))
|
if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
|
||||||
{
|
{
|
||||||
var numberIndex = nameInExtInf.IndexOf(' ');
|
var numberIndex = nameInExtInf.IndexOf(' ');
|
||||||
if (numberIndex > 0)
|
if (numberIndex > 0)
|
||||||
{
|
{
|
||||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||||
|
|
||||||
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
|
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
numberString = numberPart;
|
numberString = numberPart.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
|
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
|
||||||
|
|
||||||
if (!IsValidChannelNumber(numberString))
|
if (!IsValidChannelNumber(numberString))
|
||||||
{
|
{
|
||||||
|
@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
{
|
{
|
||||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||||
|
|
||||||
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
|
if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||||
{
|
{
|
||||||
// channel.Number = number.ToString();
|
// channel.Number = number.ToString();
|
||||||
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
|
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
|
||||||
|
|
|
@ -101,8 +101,8 @@
|
||||||
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
||||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||||
"TaskUpdatePlugins": "Update Plugins",
|
"TaskUpdatePlugins": "Update Plugins",
|
||||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
|
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||||
"TaskRefreshPeople": "Erneuere Schausteller",
|
"TaskRefreshPeople": "Erneuere Schauspieler",
|
||||||
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
||||||
"TaskCleanLogs": "Lösche Log Pfad",
|
"TaskCleanLogs": "Lösche Log Pfad",
|
||||||
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
|
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
"HeaderRecordingGroups": "錄製組",
|
"HeaderRecordingGroups": "錄製組",
|
||||||
"Inherit": "繼承",
|
"Inherit": "繼承",
|
||||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||||
"TaskRefreshChannels": "重新整理頻道",
|
"TaskRefreshChannels": "重新整理頻道",
|
||||||
"TaskUpdatePlugins": "更新插件",
|
"TaskUpdatePlugins": "更新插件",
|
||||||
|
|
|
@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try splitting by : to handle "Germany: FSK 18"
|
// Try splitting by : to handle "Germany: FSK 18"
|
||||||
var index = rating.IndexOf(':');
|
var index = rating.IndexOf(':', StringComparison.Ordinal);
|
||||||
if (index != -1)
|
if (index != -1)
|
||||||
{
|
{
|
||||||
rating = rating.Substring(index).TrimStart(':').Trim();
|
rating = rating.Substring(index).TrimStart(':').Trim();
|
||||||
|
@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
|
||||||
throw new ArgumentNullException(nameof(culture));
|
throw new ArgumentNullException(nameof(culture));
|
||||||
}
|
}
|
||||||
|
|
||||||
const string prefix = "Core";
|
const string Prefix = "Core";
|
||||||
var key = prefix + culture;
|
var key = Prefix + culture;
|
||||||
|
|
||||||
return _dictionaries.GetOrAdd(
|
return _dictionaries.GetOrAdd(
|
||||||
key,
|
key,
|
||||||
f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
|
f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
|
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
|
||||||
|
|
|
@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
|
||||||
public sealed class UdpSocket : ISocket, IDisposable
|
public sealed class UdpSocket : ISocket, IDisposable
|
||||||
{
|
{
|
||||||
private Socket _socket;
|
private Socket _socket;
|
||||||
private int _localPort;
|
private readonly int _localPort;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public Socket Socket => _socket;
|
public Socket Socket => _socket;
|
||||||
|
|
||||||
public IPAddress LocalIPAddress { get; }
|
|
||||||
|
|
||||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||||
{
|
{
|
||||||
SocketFlags = SocketFlags.None
|
SocketFlags = SocketFlags.None
|
||||||
|
@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
|
||||||
InitReceiveSocketAsyncEventArgs();
|
InitReceiveSocketAsyncEventArgs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||||
|
{
|
||||||
|
if (socket == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket = socket;
|
||||||
|
_socket.Connect(endPoint);
|
||||||
|
|
||||||
|
InitReceiveSocketAsyncEventArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPAddress LocalIPAddress { get; }
|
||||||
|
|
||||||
private void InitReceiveSocketAsyncEventArgs()
|
private void InitReceiveSocketAsyncEventArgs()
|
||||||
{
|
{
|
||||||
var receiveBuffer = new byte[8192];
|
var receiveBuffer = new byte[8192];
|
||||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
_receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
|
||||||
|
|
||||||
var sendBuffer = new byte[8192];
|
var sendBuffer = new byte[8192];
|
||||||
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
||||||
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
|
_sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||||
{
|
{
|
||||||
var tcs = _currentReceiveTaskCompletionSource;
|
var tcs = _currentReceiveTaskCompletionSource;
|
||||||
if (tcs != null)
|
if (tcs != null)
|
||||||
|
@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||||
{
|
{
|
||||||
var tcs = _currentSendTaskCompletionSource;
|
var tcs = _currentSendTaskCompletionSource;
|
||||||
if (tcs != null)
|
if (tcs != null)
|
||||||
|
@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
|
||||||
{
|
|
||||||
if (socket == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(socket));
|
|
||||||
}
|
|
||||||
|
|
||||||
_socket = socket;
|
|
||||||
_socket.Connect(endPoint);
|
|
||||||
|
|
||||||
InitReceiveSocketAsyncEventArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
|
||||||
}
|
}
|
||||||
|
|
||||||
_socket?.Dispose();
|
_socket?.Dispose();
|
||||||
|
_receiveSocketAsyncEventArgs.Dispose();
|
||||||
|
_sendSocketAsyncEventArgs.Dispose();
|
||||||
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
||||||
_currentSendTaskCompletionSource?.TrySetCanceled();
|
_currentSendTaskCompletionSource?.TrySetCanceled();
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
|
||||||
(octet[0] == 127) || // RFC1122
|
(octet[0] == 127) || // RFC1122
|
||||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||||
{
|
{
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
||||||
|
@ -390,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
|
||||||
var host = uri.DnsSafeHost;
|
var host = uri.DnsSafeHost;
|
||||||
_logger.LogDebug("Resolving host {0}", host);
|
_logger.LogDebug("Resolving host {0}", host);
|
||||||
|
|
||||||
address = GetIpAddresses(host).Result.FirstOrDefault();
|
address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
|
||||||
|
|
||||||
if (address != null)
|
if (address != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
AlbumTitle = child.Album
|
AlbumTitle = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasArtist = child as IHasArtist;
|
if (child is IHasArtist hasArtist)
|
||||||
if (hasArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
|
@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
AlbumTitle = child.Album
|
AlbumTitle = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasArtist = child as IHasArtist;
|
if (child is IHasArtist hasArtist)
|
||||||
if (hasArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
|
@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist();
|
var playlist = new M3uPlaylist
|
||||||
playlist.IsExtended = true;
|
{
|
||||||
|
IsExtended = true
|
||||||
|
};
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
{
|
{
|
||||||
var entry = new M3uPlaylistEntry()
|
var entry = new M3uPlaylistEntry()
|
||||||
|
@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
Album = child.Album
|
Album = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
|
@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
Album = child.Album
|
Album = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
|
@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
|
|
||||||
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
||||||
{
|
{
|
||||||
folderPath = folderPath + Path.DirectorySeparatorChar;
|
folderPath += Path.DirectorySeparatorChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
var folderUri = new Uri(folderPath);
|
var folderUri = new Uri(folderPath);
|
||||||
|
@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
|
||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string UnEscape(string content)
|
|
||||||
{
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Escape(string content)
|
|
||||||
{
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Folder GetPlaylistsFolder(Guid userId)
|
public Folder GetPlaylistsFolder(Guid userId)
|
||||||
{
|
{
|
||||||
var typeName = "PlaylistsFolder";
|
const string TypeName = "PlaylistsFolder";
|
||||||
|
|
||||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
|
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
|
||||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
|
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IApplicationPaths _applicationPaths;
|
private readonly IApplicationPaths _applicationPaths;
|
||||||
private readonly ILogger<TaskManager> _logger;
|
private readonly ILogger<TaskManager> _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TaskManager" /> class.
|
/// Initializes a new instance of the <see cref="TaskManager" /> class.
|
||||||
|
@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
/// <param name="applicationPaths">The application paths.</param>
|
/// <param name="applicationPaths">The application paths.</param>
|
||||||
/// <param name="jsonSerializer">The json serializer.</param>
|
/// <param name="jsonSerializer">The json serializer.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="fileSystem">The filesystem manager.</param>
|
|
||||||
public TaskManager(
|
public TaskManager(
|
||||||
IApplicationPaths applicationPaths,
|
IApplicationPaths applicationPaths,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
ILogger<TaskManager> logger,
|
ILogger<TaskManager> logger)
|
||||||
IFileSystem fileSystem)
|
|
||||||
{
|
{
|
||||||
_applicationPaths = applicationPaths;
|
_applicationPaths = applicationPaths;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
|
||||||
|
|
||||||
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
|
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.ScheduledTasks
|
namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
|
@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChapterImagesTask : IScheduledTask
|
public class ChapterImagesTask : IScheduledTask
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The _logger.
|
|
||||||
/// </summary>
|
|
||||||
private readonly ILogger<ChapterImagesTask> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _library manager.
|
/// The _library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ChapterImagesTask(
|
public ChapterImagesTask(
|
||||||
ILoggerFactory loggerFactory,
|
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
|
@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
ILocalizationManager localization)
|
ILocalizationManager localization)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger<ChapterImagesTask>();
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
if (restPath.Path[0] != '/')
|
if (restPath.Path[0] != '/')
|
||||||
{
|
{
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Route '{0}' on '{1}' must start with a '/'",
|
||||||
|
restPath.Path,
|
||||||
|
restPath.RequestType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Route '{0}' on '{1}' contains invalid chars. ",
|
||||||
|
restPath.Path,
|
||||||
|
restPath.RequestType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
||||||
|
@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
var service = httpHost.CreateInstance(serviceType);
|
var service = httpHost.CreateInstance(serviceType);
|
||||||
|
|
||||||
var serviceRequiresContext = service as IRequiresRequest;
|
if (service is IRequiresRequest serviceRequiresContext)
|
||||||
if (serviceRequiresContext != null)
|
|
||||||
{
|
{
|
||||||
serviceRequiresContext.Request = req;
|
serviceRequiresContext.Request = req;
|
||||||
}
|
}
|
||||||
|
@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
|
||||||
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
|
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
|
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
|
||||||
throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
|
throw new NotImplementedException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Could not find method named {1}({0}) or Any({0}) on Service {2}",
|
||||||
|
requestDto.GetType().GetMethodName(),
|
||||||
|
expectedMethodName,
|
||||||
|
serviceType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<object> GetTaskResult(Task task)
|
private static async Task<object> GetTaskResult(Task task)
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
var pos = pathInfo.LastIndexOf('.');
|
var pos = pathInfo.LastIndexOf('.');
|
||||||
if (pos != -1)
|
if (pos != -1)
|
||||||
{
|
{
|
||||||
var format = pathInfo.Substring(pos + 1);
|
var format = pathInfo.AsSpan().Slice(pos + 1);
|
||||||
contentType = GetFormatContentType(format);
|
contentType = GetFormatContentType(format);
|
||||||
if (contentType != null)
|
if (contentType != null)
|
||||||
{
|
{
|
||||||
|
@ -55,18 +57,21 @@ namespace Emby.Server.Implementations.Services
|
||||||
return pathInfo;
|
return pathInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetFormatContentType(string format)
|
private static string GetFormatContentType(ReadOnlySpan<char> format)
|
||||||
{
|
{
|
||||||
// built-in formats
|
if (format.Equals("json", StringComparison.Ordinal))
|
||||||
switch (format)
|
|
||||||
{
|
{
|
||||||
case "json": return "application/json";
|
return MediaTypeNames.Application.Json;
|
||||||
case "xml": return "application/xml";
|
|
||||||
default: return null;
|
|
||||||
}
|
}
|
||||||
|
else if (format.Equals("xml", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return MediaTypeNames.Application.Xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
httpReq.Items["__route"] = _restPath;
|
httpReq.Items["__route"] = _restPath;
|
||||||
|
|
||||||
|
@ -75,10 +80,11 @@ namespace Emby.Server.Implementations.Services
|
||||||
httpReq.ResponseContentType = _responseContentType;
|
httpReq.ResponseContentType = _responseContentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
|
||||||
|
|
||||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||||
|
|
||||||
|
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||||
|
|
||||||
// Apply response filters
|
// Apply response filters
|
||||||
|
@ -90,7 +96,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
|
||||||
{
|
{
|
||||||
var requestType = restPath.RequestType;
|
var requestType = restPath.RequestType;
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
var component = components[i];
|
var component = components[i];
|
||||||
|
|
||||||
if (component.StartsWith(VariablePrefix))
|
if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var variableName = component.Substring(1, component.Length - 2);
|
var variableName = component.Substring(1, component.Length - 2);
|
||||||
if (variableName[variableName.Length - 1] == WildCardChar)
|
if (variableName[variableName.Length - 1] == WildCardChar)
|
||||||
|
@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
|
||||||
sb.Append(value);
|
sb.Append(value);
|
||||||
for (var j = pathIx + 1; j < requestComponents.Length; j++)
|
for (var j = pathIx + 1; j < requestComponents.Length; j++)
|
||||||
{
|
{
|
||||||
sb.Append(PathSeperatorChar + requestComponents[j]);
|
sb.Append(PathSeperatorChar)
|
||||||
|
.Append(requestComponents[j]);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = sb.ToString();
|
value = sb.ToString();
|
||||||
|
@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
|
||||||
pathIx++;
|
pathIx++;
|
||||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
sb.Append(PathSeperatorChar)
|
||||||
|
.Append(requestComponents[pathIx++]);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = sb.ToString();
|
value = sb.ToString();
|
||||||
|
|
|
@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="info">The info.</param>
|
/// <param name="info">The info.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
/// <exception cref="ArgumentNullException">info</exception>
|
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
|
/// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
|
||||||
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
EnsureController(session, e.Argument);
|
EnsureController(session, e.Argument);
|
||||||
await KeepAliveWebSocket(e.Argument);
|
await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
// Notify WebSocket about timeout
|
// Notify WebSocket about timeout
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SendForceKeepAlive(webSocket);
|
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (WebSocketException exception)
|
catch (WebSocketException exception)
|
||||||
{
|
{
|
||||||
|
@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
if (_keepAliveCancellationToken != null)
|
if (_keepAliveCancellationToken != null)
|
||||||
{
|
{
|
||||||
_keepAliveCancellationToken.Cancel();
|
_keepAliveCancellationToken.Cancel();
|
||||||
|
_keepAliveCancellationToken.Dispose();
|
||||||
_keepAliveCancellationToken = null;
|
_keepAliveCancellationToken = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inactive.Any())
|
if (inactive.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||||
}
|
}
|
||||||
|
@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SendForceKeepAlive(webSocket);
|
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (WebSocketException exception)
|
catch (WebSocketException exception)
|
||||||
{
|
{
|
||||||
|
@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
|
|
||||||
lock (_webSocketsLock)
|
lock (_webSocketsLock)
|
||||||
{
|
{
|
||||||
if (lost.Any())
|
if (lost.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||||
foreach (var webSocket in lost)
|
foreach (var webSocket in lost)
|
||||||
|
@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_webSockets.Any())
|
if (_webSockets.Count == 0)
|
||||||
{
|
{
|
||||||
StopKeepAlive();
|
StopKeepAlive();
|
||||||
}
|
}
|
||||||
|
@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||||
{
|
{
|
||||||
return webSocket.SendAsync(new WebSocketMessage<int>
|
return webSocket.SendAsync(
|
||||||
{
|
new WebSocketMessage<int>
|
||||||
MessageType = "ForceKeepAlive",
|
{
|
||||||
Data = WebSocketLostTimeout
|
MessageType = "ForceKeepAlive",
|
||||||
}, CancellationToken.None);
|
Data = WebSocketLostTimeout
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await callback();
|
await callback().ConfigureAwait(false);
|
||||||
Task task = Task.Delay(interval, cancellationToken);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task;
|
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
|
|
|
@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
|
||||||
|
|
||||||
private static int CompareEpisodes(Episode x, Episode y)
|
private static int CompareEpisodes(Episode x, Episode y)
|
||||||
{
|
{
|
||||||
var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
|
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||||
var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
|
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||||
|
|
||||||
return xValue.CompareTo(yValue);
|
return xValue.CompareTo(yValue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
/// All sessions will receive the message.
|
/// All sessions will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AllGroup = 0,
|
AllGroup = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only the specified session will receive the message.
|
/// Only the specified session will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CurrentSession = 1,
|
CurrentSession = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All sessions, except the current one, will receive the message.
|
/// All sessions, except the current one, will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AllExceptCurrentSession = 2,
|
AllExceptCurrentSession = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only sessions that are not buffering will receive the message.
|
/// Only sessions that are not buffering will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly GroupInfo _group = new GroupInfo();
|
private readonly GroupInfo _group = new GroupInfo();
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Guid GetGroupId() => _group.GroupId;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
_syncPlayManager = syncPlayManager;
|
_syncPlayManager = syncPlayManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid GetGroupId() => _group.GroupId;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts DateTime to UTC string.
|
/// Converts DateTime to UTC string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
/// <value>The UTC string.</value>
|
/// <value>The UTC string.</value>
|
||||||
private string DateToUTCString(DateTime date)
|
private string DateToUTCString(DateTime date)
|
||||||
{
|
{
|
||||||
return date.ToUniversalTime().ToString("o");
|
return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
/// <param name="from">The current session.</param>
|
/// <param name="from">The current session.</param>
|
||||||
/// <param name="type">The filtering type.</param>
|
/// <param name="type">The filtering type.</param>
|
||||||
/// <value>The array of sessions matching the filter.</value>
|
/// <value>The array of sessions matching the filter.</value>
|
||||||
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
|
private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case BroadcastType.CurrentSession:
|
case BroadcastType.CurrentSession:
|
||||||
return new SessionInfo[] { from };
|
return new SessionInfo[] { from };
|
||||||
case BroadcastType.AllGroup:
|
case BroadcastType.AllGroup:
|
||||||
return _group.Participants.Values.Select(
|
return _group.Participants.Values
|
||||||
session => session.Session).ToArray();
|
.Select(session => session.Session);
|
||||||
case BroadcastType.AllExceptCurrentSession:
|
case BroadcastType.AllExceptCurrentSession:
|
||||||
return _group.Participants.Values.Select(
|
return _group.Participants.Values
|
||||||
session => session.Session).Where(
|
.Select(session => session.Session)
|
||||||
session => !session.Id.Equals(from.Id)).ToArray();
|
.Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
|
||||||
case BroadcastType.AllReady:
|
case BroadcastType.AllReady:
|
||||||
return _group.Participants.Values.Where(
|
return _group.Participants.Values
|
||||||
session => !session.IsBuffering).Select(
|
.Where(session => !session.IsBuffering)
|
||||||
session => session.Session).ToArray();
|
.Select(session => session.Session);
|
||||||
default:
|
default:
|
||||||
return Array.Empty<SessionInfo>();
|
return Array.Empty<SessionInfo>();
|
||||||
}
|
}
|
||||||
|
@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
{
|
{
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
SessionInfo[] sessions = FilterSessions(from, type);
|
foreach (var session in FilterSessions(from, type))
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
{
|
||||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
|
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
{
|
{
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
SessionInfo[] sessions = FilterSessions(from, type);
|
foreach (var session in FilterSessions(from, type))
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
{
|
||||||
yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
|
yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var playRequest = new PlayRequest();
|
var playRequest = new PlayRequest
|
||||||
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
|
{
|
||||||
playRequest.StartPositionTicks = _group.PositionTicks;
|
ItemIds = new Guid[] { _group.PlayingItem.Id },
|
||||||
|
StartPositionTicks = _group.PositionTicks
|
||||||
|
};
|
||||||
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
||||||
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
@ -17,15 +21,15 @@ namespace Jellyfin.Api.Controllers
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
|
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param>
|
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
||||||
public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository)
|
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
|
||||||
{
|
{
|
||||||
_displayPreferencesRepository = displayPreferencesRepository;
|
_displayPreferencesManager = displayPreferencesManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -38,12 +42,47 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
|
||||||
[HttpGet("{displayPreferencesId}")]
|
[HttpGet("{displayPreferencesId}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<DisplayPreferences> GetDisplayPreferences(
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||||
|
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
|
||||||
[FromRoute] string? displayPreferencesId,
|
[FromRoute] string? displayPreferencesId,
|
||||||
[FromQuery] [Required] string? userId,
|
[FromQuery] [Required] Guid userId,
|
||||||
[FromQuery] [Required] string? client)
|
[FromQuery] [Required] string? client)
|
||||||
{
|
{
|
||||||
return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
|
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
|
||||||
|
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
|
||||||
|
|
||||||
|
var dto = new DisplayPreferencesDto
|
||||||
|
{
|
||||||
|
Client = displayPreferences.Client,
|
||||||
|
Id = displayPreferences.UserId.ToString(),
|
||||||
|
ViewType = itemPreferences.ViewType.ToString(),
|
||||||
|
SortBy = itemPreferences.SortBy,
|
||||||
|
SortOrder = itemPreferences.SortOrder,
|
||||||
|
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||||
|
RememberIndexing = itemPreferences.RememberIndexing,
|
||||||
|
RememberSorting = itemPreferences.RememberSorting,
|
||||||
|
ScrollDirection = displayPreferences.ScrollDirection,
|
||||||
|
ShowBackdrop = displayPreferences.ShowBackdrop,
|
||||||
|
ShowSidebar = displayPreferences.ShowSidebar
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var homeSection in displayPreferences.HomeSections)
|
||||||
|
{
|
||||||
|
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
|
||||||
|
{
|
||||||
|
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||||
|
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
|
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
|
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||||
|
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||||
|
|
||||||
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -60,15 +99,77 @@ namespace Jellyfin.Api.Controllers
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||||
public ActionResult UpdateDisplayPreferences(
|
public ActionResult UpdateDisplayPreferences(
|
||||||
[FromRoute] string? displayPreferencesId,
|
[FromRoute] string? displayPreferencesId,
|
||||||
[FromQuery, BindRequired] string? userId,
|
[FromQuery, BindRequired] Guid userId,
|
||||||
[FromQuery, BindRequired] string? client,
|
[FromQuery, BindRequired] string? client,
|
||||||
[FromBody, BindRequired] DisplayPreferences displayPreferences)
|
[FromBody, BindRequired] DisplayPreferencesDto displayPreferences)
|
||||||
{
|
{
|
||||||
_displayPreferencesRepository.SaveDisplayPreferences(
|
HomeSectionType[] defaults =
|
||||||
displayPreferences,
|
{
|
||||||
userId,
|
HomeSectionType.SmallLibraryTiles,
|
||||||
client,
|
HomeSectionType.Resume,
|
||||||
CancellationToken.None);
|
HomeSectionType.ResumeAudio,
|
||||||
|
HomeSectionType.LiveTv,
|
||||||
|
HomeSectionType.NextUp,
|
||||||
|
HomeSectionType.LatestMedia, HomeSectionType.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
|
||||||
|
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
|
||||||
|
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
||||||
|
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
||||||
|
|
||||||
|
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
|
||||||
|
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
|
||||||
|
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
|
||||||
|
: ChromecastVersion.Stable;
|
||||||
|
existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
|
||||||
|
? bool.Parse(enableNextVideoInfoOverlay)
|
||||||
|
: true;
|
||||||
|
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
|
||||||
|
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
|
||||||
|
: 10000;
|
||||||
|
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
|
||||||
|
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
|
||||||
|
: 30000;
|
||||||
|
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
|
||||||
|
? theme
|
||||||
|
: string.Empty;
|
||||||
|
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
|
||||||
|
? home
|
||||||
|
: string.Empty;
|
||||||
|
existingDisplayPreferences.HomeSections.Clear();
|
||||||
|
|
||||||
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
var order = int.Parse(key.AsSpan().Slice("homesection".Length));
|
||||||
|
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||||
|
{
|
||||||
|
type = order < 7 ? defaults[order] : HomeSectionType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
|
||||||
|
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
|
||||||
|
_displayPreferencesManager.SaveChanges(itemPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
|
||||||
|
itemPrefs.SortBy = displayPreferences.SortBy;
|
||||||
|
itemPrefs.SortOrder = displayPreferences.SortOrder;
|
||||||
|
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
|
||||||
|
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||||
|
|
||||||
|
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
|
||||||
|
{
|
||||||
|
itemPrefs.ViewType = viewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
_displayPreferencesManager.SaveChanges(existingDisplayPreferences);
|
||||||
|
_displayPreferencesManager.SaveChanges(itemPrefs);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|
|
@ -53,14 +53,12 @@ namespace Jellyfin.Api.Controllers
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
|
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
|
||||||
{
|
{
|
||||||
var result = new StartupConfigurationDto
|
return new StartupConfigurationDto
|
||||||
{
|
{
|
||||||
UICulture = _config.Configuration.UICulture,
|
UICulture = _config.Configuration.UICulture,
|
||||||
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
|
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
|
||||||
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -110,10 +108,10 @@ namespace Jellyfin.Api.Controllers
|
||||||
[HttpGet("User")]
|
[HttpGet("User")]
|
||||||
[HttpGet("FirstUser")]
|
[HttpGet("FirstUser")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<StartupUserDto> GetFirstUser()
|
public async Task<StartupUserDto> GetFirstUser()
|
||||||
{
|
{
|
||||||
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
||||||
_userManager.Initialize();
|
await _userManager.InitializeAsync().ConfigureAwait(false);
|
||||||
var user = _userManager.Users.First();
|
var user = _userManager.Users.First();
|
||||||
return new StartupUserDto
|
return new StartupUserDto
|
||||||
{
|
{
|
||||||
|
|
|
@ -276,11 +276,12 @@ namespace Jellyfin.Api.Controllers
|
||||||
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
|
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AppendLine("#EXTM3U");
|
builder.AppendLine("#EXTM3U")
|
||||||
builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
|
.Append("#EXT-X-TARGETDURATION:")
|
||||||
builder.AppendLine("#EXT-X-VERSION:3");
|
.AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
|
||||||
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
|
.AppendLine("#EXT-X-VERSION:3")
|
||||||
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
|
||||||
|
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
||||||
|
|
||||||
long positionTicks = 0;
|
long positionTicks = 0;
|
||||||
|
|
||||||
|
@ -291,7 +292,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
var remaining = runtime - positionTicks;
|
var remaining = runtime - positionTicks;
|
||||||
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
|
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
|
||||||
|
|
||||||
builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
|
builder.Append("#EXTINF:")
|
||||||
|
.Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
|
||||||
|
.AppendLine(",");
|
||||||
|
|
||||||
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
|
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
@ -11,7 +12,6 @@ using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
|
@ -455,7 +455,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
|
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
|
||||||
{
|
{
|
||||||
var newUser = _userManager.CreateUser(request.Name);
|
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
|
||||||
|
|
||||||
// no need to authenticate password for new user
|
// no need to authenticate password for new user
|
||||||
if (request.Password != null)
|
if (request.Password != null)
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Models.DisplayPreferencesDtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the display preferences for any item that supports them (usually Folders).
|
||||||
|
/// </summary>
|
||||||
|
public class DisplayPreferencesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
|
||||||
|
/// </summary>
|
||||||
|
public DisplayPreferencesDto()
|
||||||
|
{
|
||||||
|
RememberIndexing = false;
|
||||||
|
PrimaryImageHeight = 250;
|
||||||
|
PrimaryImageWidth = 250;
|
||||||
|
ShowBackdrop = true;
|
||||||
|
CustomPrefs = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user id.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The user id.</value>
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the type of the view.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The type of the view.</value>
|
||||||
|
public string? ViewType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the sort by.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The sort by.</value>
|
||||||
|
public string? SortBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the index by.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The index by.</value>
|
||||||
|
public string? IndexBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether [remember indexing].
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value>
|
||||||
|
public bool RememberIndexing { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the height of the primary image.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The height of the primary image.</value>
|
||||||
|
public int PrimaryImageHeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the width of the primary image.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The width of the primary image.</value>
|
||||||
|
public int PrimaryImageWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the custom prefs.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The custom prefs.</value>
|
||||||
|
public Dictionary<string, string> CustomPrefs { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the scroll direction.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The scroll direction.</value>
|
||||||
|
public ScrollDirection ScrollDirection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to show backdrops on this item.
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value>
|
||||||
|
public bool ShowBackdrop { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether [remember sorting].
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value>
|
||||||
|
public bool RememberSorting { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the sort order.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The sort order.</value>
|
||||||
|
public SortOrder SortOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether [show sidebar].
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value>
|
||||||
|
public bool ShowSidebar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client.
|
||||||
|
/// </summary>
|
||||||
|
public string? Client { get; set; }
|
||||||
|
}
|
||||||
|
}
|
150
Jellyfin.Data/Entities/DisplayPreferences.cs
Normal file
150
Jellyfin.Data/Entities/DisplayPreferences.cs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
namespace Jellyfin.Data.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An entity representing a user's display preferences.
|
||||||
|
/// </summary>
|
||||||
|
public class DisplayPreferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user's id.</param>
|
||||||
|
/// <param name="client">The client string.</param>
|
||||||
|
public DisplayPreferences(Guid userId, string client)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
Client = client;
|
||||||
|
ShowSidebar = false;
|
||||||
|
ShowBackdrop = true;
|
||||||
|
SkipForwardLength = 30000;
|
||||||
|
SkipBackwardLength = 10000;
|
||||||
|
ScrollDirection = ScrollDirection.Horizontal;
|
||||||
|
ChromecastVersion = ChromecastVersion.Stable;
|
||||||
|
DashboardTheme = string.Empty;
|
||||||
|
TvHome = string.Empty;
|
||||||
|
|
||||||
|
HomeSections = new HashSet<HomeSection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
|
||||||
|
/// </summary>
|
||||||
|
protected DisplayPreferences()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Id.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public int Id { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user Id.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required. Max Length = 32.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(32)]
|
||||||
|
[StringLength(32)]
|
||||||
|
public string Client { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to show the sidebar.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public bool ShowSidebar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to show the backdrop.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public bool ShowBackdrop { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the scroll direction.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public ScrollDirection ScrollDirection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets what the view should be indexed by.
|
||||||
|
/// </summary>
|
||||||
|
public IndexingKind? IndexBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the length of time to skip forwards, in milliseconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public int SkipForwardLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the length of time to skip backwards, in milliseconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public int SkipBackwardLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Chromecast Version.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public ChromecastVersion ChromecastVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the next video info overlay should be shown.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public bool EnableNextVideoInfoOverlay { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the dashboard theme.
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(32)]
|
||||||
|
[StringLength(32)]
|
||||||
|
public string DashboardTheme { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the tv home screen.
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(32)]
|
||||||
|
[StringLength(32)]
|
||||||
|
public string TvHome { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the home sections.
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<HomeSection> HomeSections { get; protected set; }
|
||||||
|
}
|
||||||
|
}
|
46
Jellyfin.Data/Entities/HomeSection.cs
Normal file
46
Jellyfin.Data/Entities/HomeSection.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
namespace Jellyfin.Data.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An entity representing a section on the user's home page.
|
||||||
|
/// </summary>
|
||||||
|
public class HomeSection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Identity. Required.
|
||||||
|
/// </remarks>
|
||||||
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public int Id { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Id of the associated display preferences.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public int DisplayPreferencesId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the order.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public HomeSectionType Type { get; set; }
|
||||||
|
}
|
||||||
|
}
|
120
Jellyfin.Data/Entities/ItemDisplayPreferences.cs
Normal file
120
Jellyfin.Data/Entities/ItemDisplayPreferences.cs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
namespace Jellyfin.Data.Entities
|
||||||
|
{
|
||||||
|
public class ItemDisplayPreferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user id.</param>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
ItemId = itemId;
|
||||||
|
Client = client;
|
||||||
|
|
||||||
|
SortBy = "SortName";
|
||||||
|
ViewType = ViewType.Poster;
|
||||||
|
SortOrder = SortOrder.Ascending;
|
||||||
|
RememberSorting = false;
|
||||||
|
RememberIndexing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
|
||||||
|
/// </summary>
|
||||||
|
protected ItemDisplayPreferences()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Id.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public int Id { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user Id.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id of the associated item.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public Guid ItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required. Max Length = 32.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(32)]
|
||||||
|
[StringLength(32)]
|
||||||
|
public string Client { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the view type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public ViewType ViewType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the indexing should be remembered.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public bool RememberIndexing { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets what the view should be indexed by.
|
||||||
|
/// </summary>
|
||||||
|
public IndexingKind? IndexBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the sorting type should be remembered.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public bool RememberSorting { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets what the view should be sorted by.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(64)]
|
||||||
|
[StringLength(64)]
|
||||||
|
public string SortBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the sort order.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
public SortOrder SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
|
||||||
PasswordResetProviderId = passwordResetProviderId;
|
PasswordResetProviderId = passwordResetProviderId;
|
||||||
|
|
||||||
AccessSchedules = new HashSet<AccessSchedule>();
|
AccessSchedules = new HashSet<AccessSchedule>();
|
||||||
|
ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
|
||||||
// Groups = new HashSet<Group>();
|
// Groups = new HashSet<Group>();
|
||||||
Permissions = new HashSet<Permission>();
|
Permissions = new HashSet<Permission>();
|
||||||
Preferences = new HashSet<Preference>();
|
Preferences = new HashSet<Preference>();
|
||||||
|
@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
|
||||||
// [ForeignKey("UserId")]
|
// [ForeignKey("UserId")]
|
||||||
public virtual ImageInfo ProfileImage { get; set; }
|
public virtual ImageInfo ProfileImage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user's display preferences.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Required.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
public virtual DisplayPreferences DisplayPreferences { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public SyncPlayAccess SyncPlayAccess { get; set; }
|
public SyncPlayAccess SyncPlayAccess { get; set; }
|
||||||
|
|
||||||
|
@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
|
public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of item display preferences.
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of groups this user is a member of.
|
/// Gets or sets the list of groups this user is a member of.
|
||||||
|
|
18
Jellyfin.Data/Enums/ChromecastVersion.cs
Normal file
18
Jellyfin.Data/Enums/ChromecastVersion.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An enum representing the version of Chromecast to be used by clients.
|
||||||
|
/// </summary>
|
||||||
|
public enum ChromecastVersion
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stable Chromecast version.
|
||||||
|
/// </summary>
|
||||||
|
Stable = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unstable Chromecast version.
|
||||||
|
/// </summary>
|
||||||
|
Unstable = 1
|
||||||
|
}
|
||||||
|
}
|
53
Jellyfin.Data/Enums/HomeSectionType.cs
Normal file
53
Jellyfin.Data/Enums/HomeSectionType.cs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An enum representing the different options for the home screen sections.
|
||||||
|
/// </summary>
|
||||||
|
public enum HomeSectionType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// None.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// My Media.
|
||||||
|
/// </summary>
|
||||||
|
SmallLibraryTiles = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// My Media Small.
|
||||||
|
/// </summary>
|
||||||
|
LibraryButtons = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Active Recordings.
|
||||||
|
/// </summary>
|
||||||
|
ActiveRecordings = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Continue Watching.
|
||||||
|
/// </summary>
|
||||||
|
Resume = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Continue Listening.
|
||||||
|
/// </summary>
|
||||||
|
ResumeAudio = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Latest Media.
|
||||||
|
/// </summary>
|
||||||
|
LatestMedia = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Next Up.
|
||||||
|
/// </summary>
|
||||||
|
NextUp = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live TV.
|
||||||
|
/// </summary>
|
||||||
|
LiveTv = 8
|
||||||
|
}
|
||||||
|
}
|
20
Jellyfin.Data/Enums/IndexingKind.cs
Normal file
20
Jellyfin.Data/Enums/IndexingKind.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
public enum IndexingKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Index by the premiere date.
|
||||||
|
/// </summary>
|
||||||
|
PremiereDate = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index by the production year.
|
||||||
|
/// </summary>
|
||||||
|
ProductionYear = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index by the community rating.
|
||||||
|
/// </summary>
|
||||||
|
CommunityRating = 2
|
||||||
|
}
|
||||||
|
}
|
18
Jellyfin.Data/Enums/ScrollDirection.cs
Normal file
18
Jellyfin.Data/Enums/ScrollDirection.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An enum representing the axis that should be scrolled.
|
||||||
|
/// </summary>
|
||||||
|
public enum ScrollDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Horizontal scrolling direction.
|
||||||
|
/// </summary>
|
||||||
|
Horizontal = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vertical scrolling direction.
|
||||||
|
/// </summary>
|
||||||
|
Vertical = 1
|
||||||
|
}
|
||||||
|
}
|
18
Jellyfin.Data/Enums/SortOrder.cs
Normal file
18
Jellyfin.Data/Enums/SortOrder.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An enum representing the sorting order.
|
||||||
|
/// </summary>
|
||||||
|
public enum SortOrder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort in increasing order.
|
||||||
|
/// </summary>
|
||||||
|
Ascending = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort in decreasing order.
|
||||||
|
/// </summary>
|
||||||
|
Descending = 1
|
||||||
|
}
|
||||||
|
}
|
38
Jellyfin.Data/Enums/ViewType.cs
Normal file
38
Jellyfin.Data/Enums/ViewType.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
namespace Jellyfin.Data.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An enum representing the type of view for a library or collection.
|
||||||
|
/// </summary>
|
||||||
|
public enum ViewType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shows banners.
|
||||||
|
/// </summary>
|
||||||
|
Banner = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a list of content.
|
||||||
|
/// </summary>
|
||||||
|
List = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows poster artwork.
|
||||||
|
/// </summary>
|
||||||
|
Poster = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows poster artwork with a card containing the name and year.
|
||||||
|
/// </summary>
|
||||||
|
PosterCard = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a thumbnail.
|
||||||
|
/// </summary>
|
||||||
|
Thumb = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a thumbnail with a card containing the name and year.
|
||||||
|
/// </summary>
|
||||||
|
ThumbCard = 5
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,11 +18,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BlurHashSharp" Version="1.0.1" />
|
<PackageReference Include="BlurHashSharp" Version="1.1.0" />
|
||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
|
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="1.68.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.1" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
|
||||||
<PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// <param name="percent">The percentage played to display with the indicator.</param>
|
/// <param name="percent">The percentage played to display with the indicator.</param>
|
||||||
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
||||||
{
|
{
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint();
|
||||||
{
|
var endX = imageSize.Width - 1;
|
||||||
var endX = imageSize.Width - 1;
|
var endY = imageSize.Height - 1;
|
||||||
var endY = imageSize.Height - 1;
|
|
||||||
|
|
||||||
paint.Color = SKColor.Parse("#99000000");
|
paint.Color = SKColor.Parse("#99000000");
|
||||||
paint.Style = SKPaintStyle.Fill;
|
paint.Style = SKPaintStyle.Fill;
|
||||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
|
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
|
||||||
|
|
||||||
double foregroundWidth = endX;
|
double foregroundWidth = (endX * percent) / 100;
|
||||||
foregroundWidth *= percent;
|
|
||||||
foregroundWidth /= 100;
|
|
||||||
|
|
||||||
paint.Color = SKColor.Parse("#FF00A4DC");
|
paint.Color = SKColor.Parse("#FF00A4DC");
|
||||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
|
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia
|
||||||
{
|
{
|
||||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
Color = SKColor.Parse("#CC00A4DC"),
|
||||||
paint.Style = SKPaintStyle.Fill;
|
Style = SKPaintStyle.Fill
|
||||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||||
{
|
|
||||||
paint.Color = new SKColor(255, 255, 255, 255);
|
|
||||||
paint.Style = SKPaintStyle.Fill;
|
|
||||||
|
|
||||||
paint.TextSize = 30;
|
paint.Color = new SKColor(255, 255, 255, 255);
|
||||||
paint.IsAntialias = true;
|
paint.TextSize = 30;
|
||||||
|
paint.IsAntialias = true;
|
||||||
|
|
||||||
// or:
|
// or:
|
||||||
// var emojiChar = 0x1F680;
|
// var emojiChar = 0x1F680;
|
||||||
const string Text = "✔️";
|
const string Text = "✔️";
|
||||||
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
||||||
|
|
||||||
// ask the font manager for a font with that character
|
// ask the font manager for a font with that character
|
||||||
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
||||||
|
|
||||||
canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
|
canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||||
public SkiaCodecException(SKCodecResult result) : base()
|
public SkiaCodecException(SKCodecResult result)
|
||||||
{
|
{
|
||||||
CodecResult = result;
|
CodecResult = result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The application logger.</param>
|
/// <param name="logger">The application logger.</param>
|
||||||
/// <param name="appPaths">The application paths.</param>
|
/// <param name="appPaths">The application paths.</param>
|
||||||
public SkiaEncoder(
|
public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
|
||||||
ILogger<SkiaEncoder> logger,
|
|
||||||
IApplicationPaths appPaths)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
|
@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// <returns>The converted format.</returns>
|
/// <returns>The converted format.</returns>
|
||||||
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
||||||
{
|
{
|
||||||
switch (selectedFormat)
|
return selectedFormat switch
|
||||||
{
|
{
|
||||||
case ImageFormat.Bmp:
|
ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
|
||||||
return SKEncodedImageFormat.Bmp;
|
ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
|
||||||
case ImageFormat.Jpg:
|
ImageFormat.Gif => SKEncodedImageFormat.Gif,
|
||||||
return SKEncodedImageFormat.Jpeg;
|
ImageFormat.Webp => SKEncodedImageFormat.Webp,
|
||||||
case ImageFormat.Gif:
|
_ => SKEncodedImageFormat.Png
|
||||||
return SKEncodedImageFormat.Gif;
|
};
|
||||||
case ImageFormat.Webp:
|
|
||||||
return SKEncodedImageFormat.Webp;
|
|
||||||
default:
|
|
||||||
return SKEncodedImageFormat.Png;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsTransparentRow(SKBitmap bmp, int row)
|
private static bool IsTransparentRow(SKBitmap bmp, int row)
|
||||||
|
@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
|
||||||
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
|
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
|
||||||
{
|
{
|
||||||
var topmost = 0;
|
var topmost = 0;
|
||||||
for (int row = 0; row < bitmap.Height; ++row)
|
while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
|
||||||
{
|
{
|
||||||
if (IsTransparentRow(bitmap, row))
|
topmost++;
|
||||||
{
|
|
||||||
topmost = row + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int bottommost = bitmap.Height;
|
int bottommost = bitmap.Height;
|
||||||
for (int row = bitmap.Height - 1; row >= 0; --row)
|
while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1))
|
||||||
{
|
{
|
||||||
if (IsTransparentRow(bitmap, row))
|
bottommost--;
|
||||||
{
|
|
||||||
bottommost = row;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int leftmost = 0, rightmost = bitmap.Width;
|
var leftmost = 0;
|
||||||
for (int col = 0; col < bitmap.Width; ++col)
|
while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
|
||||||
{
|
{
|
||||||
if (IsTransparentColumn(bitmap, col))
|
leftmost++;
|
||||||
{
|
|
||||||
leftmost = col + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int col = bitmap.Width - 1; col >= 0; --col)
|
var rightmost = bitmap.Width;
|
||||||
|
while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1))
|
||||||
{
|
{
|
||||||
if (IsTransparentColumn(bitmap, col))
|
rightmost--;
|
||||||
{
|
|
||||||
rightmost = col;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
|
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
|
||||||
|
|
||||||
using (var image = SKImage.FromBitmap(bitmap))
|
using var image = SKImage.FromBitmap(bitmap);
|
||||||
using (var subset = image.Subset(newRect))
|
using var subset = image.Subset(newRect);
|
||||||
{
|
return SKBitmap.FromImage(subset);
|
||||||
return SKBitmap.FromImage(subset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
|
||||||
throw new FileNotFoundException("File not found", path);
|
throw new FileNotFoundException("File not found", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var codec = SKCodec.Create(path, out SKCodecResult result))
|
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||||
{
|
EnsureSuccess(result);
|
||||||
EnsureSuccess(result);
|
|
||||||
|
|
||||||
var info = codec.Info;
|
var info = codec.Info;
|
||||||
|
|
||||||
return new ImageDimensions(info.Width, info.Height);
|
return new ImageDimensions(info.Width, info.Height);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -237,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
return BlurHashEncoder.Encode(xComp, yComp, path);
|
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||||
|
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasDiacritics(string text)
|
private static bool HasDiacritics(string text)
|
||||||
|
@ -253,12 +216,7 @@ namespace Jellyfin.Drawing.Skia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasDiacritics(path))
|
return HasDiacritics(path);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string NormalizePath(string path)
|
private string NormalizePath(string path)
|
||||||
|
@ -283,25 +241,17 @@ namespace Jellyfin.Drawing.Skia
|
||||||
return SKEncodedOrigin.TopLeft;
|
return SKEncodedOrigin.TopLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (orientation.Value)
|
return orientation.Value switch
|
||||||
{
|
{
|
||||||
case ImageOrientation.TopRight:
|
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
||||||
return SKEncodedOrigin.TopRight;
|
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
||||||
case ImageOrientation.RightTop:
|
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
||||||
return SKEncodedOrigin.RightTop;
|
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
||||||
case ImageOrientation.RightBottom:
|
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
||||||
return SKEncodedOrigin.RightBottom;
|
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
||||||
case ImageOrientation.LeftTop:
|
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
||||||
return SKEncodedOrigin.LeftTop;
|
_ => SKEncodedOrigin.TopLeft
|
||||||
case ImageOrientation.LeftBottom:
|
};
|
||||||
return SKEncodedOrigin.LeftBottom;
|
|
||||||
case ImageOrientation.BottomRight:
|
|
||||||
return SKEncodedOrigin.BottomRight;
|
|
||||||
case ImageOrientation.BottomLeft:
|
|
||||||
return SKEncodedOrigin.BottomLeft;
|
|
||||||
default:
|
|
||||||
return SKEncodedOrigin.TopLeft;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -323,24 +273,22 @@ namespace Jellyfin.Drawing.Skia
|
||||||
|
|
||||||
if (requiresTransparencyHack || forceCleanBitmap)
|
if (requiresTransparencyHack || forceCleanBitmap)
|
||||||
{
|
{
|
||||||
using (var codec = SKCodec.Create(NormalizePath(path)))
|
using var codec = SKCodec.Create(NormalizePath(path));
|
||||||
|
if (codec == null)
|
||||||
{
|
{
|
||||||
if (codec == null)
|
origin = GetSKEncodedOrigin(orientation);
|
||||||
{
|
return null;
|
||||||
origin = GetSKEncodedOrigin(orientation);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the bitmap
|
|
||||||
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
|
||||||
|
|
||||||
// decode
|
|
||||||
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
|
||||||
|
|
||||||
origin = codec.EncodedOrigin;
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the bitmap
|
||||||
|
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
||||||
|
|
||||||
|
// decode
|
||||||
|
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
||||||
|
|
||||||
|
origin = codec.EncodedOrigin;
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
||||||
|
@ -367,15 +315,8 @@ namespace Jellyfin.Drawing.Skia
|
||||||
{
|
{
|
||||||
if (cropWhitespace)
|
if (cropWhitespace)
|
||||||
{
|
{
|
||||||
using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
|
using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
||||||
{
|
return bitmap == null ? null : CropWhiteSpace(bitmap);
|
||||||
if (bitmap == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CropWhiteSpace(bitmap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
||||||
|
@ -403,133 +344,105 @@ namespace Jellyfin.Drawing.Skia
|
||||||
|
|
||||||
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
||||||
{
|
{
|
||||||
|
if (origin == SKEncodedOrigin.Default)
|
||||||
|
{
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsFlip = origin == SKEncodedOrigin.LeftBottom
|
||||||
|
|| origin == SKEncodedOrigin.LeftTop
|
||||||
|
|| origin == SKEncodedOrigin.RightBottom
|
||||||
|
|| origin == SKEncodedOrigin.RightTop;
|
||||||
|
var rotated = needsFlip
|
||||||
|
? new SKBitmap(bitmap.Height, bitmap.Width)
|
||||||
|
: new SKBitmap(bitmap.Width, bitmap.Height);
|
||||||
|
using var surface = new SKCanvas(rotated);
|
||||||
|
var midX = (float)rotated.Width / 2;
|
||||||
|
var midY = (float)rotated.Height / 2;
|
||||||
|
|
||||||
switch (origin)
|
switch (origin)
|
||||||
{
|
{
|
||||||
case SKEncodedOrigin.TopRight:
|
case SKEncodedOrigin.TopRight:
|
||||||
{
|
surface.Scale(-1, 1, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.Scale(-1, 1);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.BottomRight:
|
case SKEncodedOrigin.BottomRight:
|
||||||
{
|
surface.RotateDegrees(180, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
float px = (float)bitmap.Width / 2;
|
|
||||||
float py = (float)bitmap.Height / 2;
|
|
||||||
|
|
||||||
surface.RotateDegrees(180, px, py);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.BottomLeft:
|
case SKEncodedOrigin.BottomLeft:
|
||||||
{
|
surface.Scale(1, -1, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
float px = (float)bitmap.Width / 2;
|
|
||||||
|
|
||||||
float py = (float)bitmap.Height / 2;
|
|
||||||
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.Scale(-1, 1);
|
|
||||||
|
|
||||||
surface.RotateDegrees(180, px, py);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.LeftTop:
|
case SKEncodedOrigin.LeftTop:
|
||||||
{
|
surface.Translate(0, -rotated.Height);
|
||||||
// TODO: Remove dual canvases, had trouble with flipping
|
surface.Scale(1, -1, midX, midY);
|
||||||
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
|
surface.RotateDegrees(-90);
|
||||||
{
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
|
|
||||||
surface.RotateDegrees(90);
|
|
||||||
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
|
|
||||||
using (var flippedCanvas = new SKCanvas(flippedBitmap))
|
|
||||||
{
|
|
||||||
flippedCanvas.Translate(flippedBitmap.Width, 0);
|
|
||||||
flippedCanvas.Scale(-1, 1);
|
|
||||||
flippedCanvas.DrawBitmap(rotated, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flippedBitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.RightTop:
|
case SKEncodedOrigin.RightTop:
|
||||||
{
|
surface.Translate(rotated.Width, 0);
|
||||||
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
|
surface.RotateDegrees(90);
|
||||||
using (var surface = new SKCanvas(rotated))
|
break;
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.RotateDegrees(90);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.RightBottom:
|
case SKEncodedOrigin.RightBottom:
|
||||||
{
|
surface.Translate(rotated.Width, 0);
|
||||||
// TODO: Remove dual canvases, had trouble with flipping
|
surface.Scale(1, -1, midX, midY);
|
||||||
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
|
surface.RotateDegrees(90);
|
||||||
{
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(0, rotated.Height);
|
|
||||||
surface.RotateDegrees(270);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
|
|
||||||
using (var flippedCanvas = new SKCanvas(flippedBitmap))
|
|
||||||
{
|
|
||||||
flippedCanvas.Translate(flippedBitmap.Width, 0);
|
|
||||||
flippedCanvas.Scale(-1, 1);
|
|
||||||
flippedCanvas.DrawBitmap(rotated, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flippedBitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.LeftBottom:
|
case SKEncodedOrigin.LeftBottom:
|
||||||
{
|
surface.Translate(0, rotated.Height);
|
||||||
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
|
surface.RotateDegrees(-90);
|
||||||
using (var surface = new SKCanvas(rotated))
|
break;
|
||||||
{
|
|
||||||
surface.Translate(0, rotated.Height);
|
|
||||||
surface.RotateDegrees(270);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface.DrawBitmap(bitmap, 0, 0);
|
||||||
|
return rotated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resizes an image on the CPU, by utilizing a surface and canvas.
|
||||||
|
///
|
||||||
|
/// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
|
||||||
|
/// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source bitmap.</param>
|
||||||
|
/// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
|
||||||
|
/// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
|
||||||
|
/// <param name="isDither">This enables dithering on the SKPaint instance.</param>
|
||||||
|
/// <returns>The resized image.</returns>
|
||||||
|
internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
|
||||||
|
{
|
||||||
|
using var surface = SKSurface.Create(targetInfo);
|
||||||
|
using var canvas = surface.Canvas;
|
||||||
|
using var paint = new SKPaint
|
||||||
|
{
|
||||||
|
FilterQuality = SKFilterQuality.High,
|
||||||
|
IsAntialias = isAntialias,
|
||||||
|
IsDither = isDither
|
||||||
|
};
|
||||||
|
|
||||||
|
var kernel = new float[9]
|
||||||
|
{
|
||||||
|
0, -.1f, 0,
|
||||||
|
-.1f, 1.4f, -.1f,
|
||||||
|
0, -.1f, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var kernelSize = new SKSizeI(3, 3);
|
||||||
|
var kernelOffset = new SKPointI(1, 1);
|
||||||
|
|
||||||
|
paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||||
|
kernelSize,
|
||||||
|
kernel,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
kernelOffset,
|
||||||
|
SKShaderTileMode.Clamp,
|
||||||
|
false);
|
||||||
|
|
||||||
|
canvas.DrawBitmap(
|
||||||
|
source,
|
||||||
|
SKRect.Create(0, 0, source.Width, source.Height),
|
||||||
|
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
|
||||||
|
paint);
|
||||||
|
|
||||||
|
return surface.Snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -552,97 +465,87 @@ namespace Jellyfin.Drawing.Skia
|
||||||
var blur = options.Blur ?? 0;
|
var blur = options.Blur ?? 0;
|
||||||
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
||||||
|
|
||||||
using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation))
|
using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation);
|
||||||
|
if (bitmap == null)
|
||||||
{
|
{
|
||||||
if (bitmap == null)
|
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
||||||
|
|
||||||
|
if (!options.CropWhiteSpace
|
||||||
|
&& options.HasDefaultOptions(inputPath, originalImageSize)
|
||||||
|
&& !autoOrient)
|
||||||
|
{
|
||||||
|
// Just spit out the original file if all the options are default
|
||||||
|
return inputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
||||||
|
|
||||||
|
var width = newImageSize.Width;
|
||||||
|
var height = newImageSize.Height;
|
||||||
|
|
||||||
|
// scale image (the FromImage creates a copy)
|
||||||
|
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
|
||||||
|
using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
|
||||||
|
|
||||||
|
// If all we're doing is resizing then we can stop now
|
||||||
|
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||||
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
|
||||||
|
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create bitmap to use for canvas drawing used to draw into bitmap
|
||||||
|
using var saveBitmap = new SKBitmap(width, height);
|
||||||
|
using var canvas = new SKCanvas(saveBitmap);
|
||||||
|
// set background color if present
|
||||||
|
if (hasBackgroundColor)
|
||||||
|
{
|
||||||
|
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add blur if option is present
|
||||||
|
if (blur > 0)
|
||||||
|
{
|
||||||
|
// create image from resized bitmap to apply blur
|
||||||
|
using var paint = new SKPaint();
|
||||||
|
using var filter = SKImageFilter.CreateBlur(blur, blur);
|
||||||
|
paint.ImageFilter = filter;
|
||||||
|
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// draw resized bitmap onto canvas
|
||||||
|
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If foreground layer present then draw
|
||||||
|
if (hasForegroundColor)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
opacity = .4;
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.CropWhiteSpace
|
if (hasIndicator)
|
||||||
&& options.HasDefaultOptions(inputPath, originalImageSize)
|
{
|
||||||
&& !autoOrient)
|
DrawIndicator(canvas, width, height, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||||
|
using (var outputStream = new SKFileWStream(outputPath))
|
||||||
|
{
|
||||||
|
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
||||||
{
|
{
|
||||||
// Just spit out the original file if all the options are default
|
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||||
return inputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
|
||||||
|
|
||||||
var width = newImageSize.Width;
|
|
||||||
var height = newImageSize.Height;
|
|
||||||
|
|
||||||
using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
|
|
||||||
{
|
|
||||||
// scale image
|
|
||||||
bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// If all we're doing is resizing then we can stop now
|
|
||||||
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
|
|
||||||
{
|
|
||||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create bitmap to use for canvas drawing used to draw into bitmap
|
|
||||||
using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType))
|
|
||||||
using (var canvas = new SKCanvas(saveBitmap))
|
|
||||||
{
|
|
||||||
// set background color if present
|
|
||||||
if (hasBackgroundColor)
|
|
||||||
{
|
|
||||||
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add blur if option is present
|
|
||||||
if (blur > 0)
|
|
||||||
{
|
|
||||||
// create image from resized bitmap to apply blur
|
|
||||||
using (var paint = new SKPaint())
|
|
||||||
using (var filter = SKImageFilter.CreateBlur(blur, blur))
|
|
||||||
{
|
|
||||||
paint.ImageFilter = filter;
|
|
||||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// draw resized bitmap onto canvas
|
|
||||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If foreground layer present then draw
|
|
||||||
if (hasForegroundColor)
|
|
||||||
{
|
|
||||||
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
|
||||||
{
|
|
||||||
opacity = .4;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasIndicator)
|
|
||||||
{
|
|
||||||
DrawIndicator(canvas, width, height, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
|
||||||
{
|
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
|
||||||
{
|
|
||||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SkiaException() : base()
|
public SkiaException()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// <param name="height">The desired height of the collage.</param>
|
/// <param name="height">The desired height of the collage.</param>
|
||||||
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
|
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
|
||||||
{
|
{
|
||||||
using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
|
using var bitmap = BuildSquareCollageBitmap(paths, width, height);
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||||
{
|
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -86,56 +84,44 @@ namespace Jellyfin.Drawing.Skia
|
||||||
/// <param name="height">The desired height of the collage.</param>
|
/// <param name="height">The desired height of the collage.</param>
|
||||||
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
|
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
|
||||||
{
|
{
|
||||||
using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
|
using var bitmap = BuildThumbCollageBitmap(paths, width, height);
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||||
{
|
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
|
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
|
||||||
{
|
{
|
||||||
var bitmap = new SKBitmap(width, height);
|
var bitmap = new SKBitmap(width, height);
|
||||||
|
|
||||||
using (var canvas = new SKCanvas(bitmap))
|
using var canvas = new SKCanvas(bitmap);
|
||||||
|
canvas.Clear(SKColors.Black);
|
||||||
|
|
||||||
|
// number of images used in the thumbnail
|
||||||
|
var iCount = 3;
|
||||||
|
|
||||||
|
// determine sizes for each image that will composited into the final image
|
||||||
|
var iSlice = Convert.ToInt32(width / iCount);
|
||||||
|
int iHeight = Convert.ToInt32(height * 1.00);
|
||||||
|
int imageIndex = 0;
|
||||||
|
for (int i = 0; i < iCount; i++)
|
||||||
{
|
{
|
||||||
canvas.Clear(SKColors.Black);
|
using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
|
||||||
|
imageIndex = newIndex;
|
||||||
// number of images used in the thumbnail
|
if (currentBitmap == null)
|
||||||
var iCount = 3;
|
|
||||||
|
|
||||||
// determine sizes for each image that will composited into the final image
|
|
||||||
var iSlice = Convert.ToInt32(width / iCount);
|
|
||||||
int iHeight = Convert.ToInt32(height * 1.00);
|
|
||||||
int imageIndex = 0;
|
|
||||||
for (int i = 0; i < iCount; i++)
|
|
||||||
{
|
{
|
||||||
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
|
continue;
|
||||||
{
|
|
||||||
imageIndex = newIndex;
|
|
||||||
if (currentBitmap == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize to the same aspect as the original
|
|
||||||
int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
|
|
||||||
using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
|
|
||||||
{
|
|
||||||
currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// crop image
|
|
||||||
int ix = Math.Abs((iWidth - iSlice) / 2);
|
|
||||||
using (var image = SKImage.FromBitmap(resizeBitmap))
|
|
||||||
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
|
|
||||||
{
|
|
||||||
// draw image onto canvas
|
|
||||||
canvas.DrawImage(subset ?? image, iSlice * i, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resize to the same aspect as the original
|
||||||
|
int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
|
||||||
|
using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
|
||||||
|
|
||||||
|
// crop image
|
||||||
|
int ix = Math.Abs((iWidth - iSlice) / 2);
|
||||||
|
using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
|
||||||
|
// draw image onto canvas
|
||||||
|
canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
|
@ -176,33 +162,27 @@ namespace Jellyfin.Drawing.Skia
|
||||||
var cellWidth = width / 2;
|
var cellWidth = width / 2;
|
||||||
var cellHeight = height / 2;
|
var cellHeight = height / 2;
|
||||||
|
|
||||||
using (var canvas = new SKCanvas(bitmap))
|
using var canvas = new SKCanvas(bitmap);
|
||||||
|
for (var x = 0; x < 2; x++)
|
||||||
{
|
{
|
||||||
for (var x = 0; x < 2; x++)
|
for (var y = 0; y < 2; y++)
|
||||||
{
|
{
|
||||||
for (var y = 0; y < 2; y++)
|
using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
|
||||||
|
imageIndex = newIndex;
|
||||||
|
|
||||||
|
if (currentBitmap == null)
|
||||||
{
|
{
|
||||||
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
|
continue;
|
||||||
{
|
|
||||||
imageIndex = newIndex;
|
|
||||||
|
|
||||||
if (currentBitmap == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
|
|
||||||
{
|
|
||||||
// scale image
|
|
||||||
currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// draw this image into the strip at the next position
|
|
||||||
var xPos = x * cellWidth;
|
|
||||||
var yPos = y * cellHeight;
|
|
||||||
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale image. The FromBitmap creates a copy
|
||||||
|
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
|
||||||
|
using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo));
|
||||||
|
|
||||||
|
// draw this image into the strip at the next position
|
||||||
|
var xPos = x * cellWidth;
|
||||||
|
var yPos = y * cellHeight;
|
||||||
|
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia
|
||||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||||
var text = count.ToString(CultureInfo.InvariantCulture);
|
var text = count.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
Color = SKColor.Parse("#CC00A4DC"),
|
||||||
paint.Style = SKPaintStyle.Fill;
|
Style = SKPaintStyle.Fill
|
||||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
};
|
||||||
|
|
||||||
|
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||||
|
|
||||||
|
paint.Color = new SKColor(255, 255, 255, 255);
|
||||||
|
paint.TextSize = 24;
|
||||||
|
paint.IsAntialias = true;
|
||||||
|
|
||||||
|
var y = OffsetFromTopRightCorner + 9;
|
||||||
|
|
||||||
|
if (text.Length == 1)
|
||||||
|
{
|
||||||
|
x -= 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
if (text.Length == 2)
|
||||||
{
|
{
|
||||||
paint.Color = new SKColor(255, 255, 255, 255);
|
x -= 13;
|
||||||
paint.Style = SKPaintStyle.Fill;
|
|
||||||
|
|
||||||
paint.TextSize = 24;
|
|
||||||
paint.IsAntialias = true;
|
|
||||||
|
|
||||||
var y = OffsetFromTopRightCorner + 9;
|
|
||||||
|
|
||||||
if (text.Length == 1)
|
|
||||||
{
|
|
||||||
x -= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.Length == 2)
|
|
||||||
{
|
|
||||||
x -= 13;
|
|
||||||
}
|
|
||||||
else if (text.Length >= 3)
|
|
||||||
{
|
|
||||||
x -= 15;
|
|
||||||
y -= 2;
|
|
||||||
paint.TextSize = 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.DrawText(text, x, y, paint);
|
|
||||||
}
|
}
|
||||||
|
else if (text.Length >= 3)
|
||||||
|
{
|
||||||
|
x -= 15;
|
||||||
|
y -= 2;
|
||||||
|
paint.TextSize = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.DrawText(text, x, y, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data;
|
using Jellyfin.Data;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -27,8 +28,12 @@ namespace Jellyfin.Server.Implementations
|
||||||
|
|
||||||
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
|
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
|
||||||
|
|
||||||
|
public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
|
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
|
||||||
|
|
||||||
|
public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<Permission> Permissions { get; set; }
|
public virtual DbSet<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<Preference> Preferences { get; set; }
|
public virtual DbSet<Preference> Preferences { get; set; }
|
||||||
|
@ -133,6 +138,18 @@ namespace Jellyfin.Server.Implementations
|
||||||
return base.SaveChanges();
|
return base.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var entry in ChangeTracker.Entries())
|
||||||
|
{
|
||||||
|
entry.State = EntityState.Detached;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations
|
||||||
public class JellyfinDbProvider
|
public class JellyfinDbProvider
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IApplicationPaths _appPaths;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
|
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider">The application's service provider.</param>
|
/// <param name="serviceProvider">The application's service provider.</param>
|
||||||
public JellyfinDbProvider(IServiceProvider serviceProvider)
|
/// <param name="appPaths">The application paths.</param>
|
||||||
|
public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
|
_appPaths = appPaths;
|
||||||
|
|
||||||
|
using var jellyfinDb = CreateContext();
|
||||||
|
jellyfinDb.Database.Migrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations
|
||||||
/// <returns>The newly created context.</returns>
|
/// <returns>The newly created context.</returns>
|
||||||
public JellyfinDb CreateContext()
|
public JellyfinDb CreateContext()
|
||||||
{
|
{
|
||||||
return _serviceProvider.GetRequiredService<JellyfinDb>();
|
var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
|
||||||
|
return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
459
Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
generated
Normal file
459
Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
generated
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
#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("20200728005145_AddDisplayPreferences")]
|
||||||
|
partial class AddDisplayPreferences
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("jellyfin")
|
||||||
|
.HasAnnotation("ProductVersion", "3.1.6");
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<int>("LogSeverity")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(512);
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(512);
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ShortOverview")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(512);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<string>("DashboardTheme")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("IndexBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(512);
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(64);
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(65535);
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(255);
|
||||||
|
|
||||||
|
b.Property<string>("AuthenticationProviderId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(255);
|
||||||
|
|
||||||
|
b.Property<bool>("DisplayCollectionsView")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DisplayMissingEpisodes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("EasyPassword")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(65535);
|
||||||
|
|
||||||
|
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?>("MaxParentalAgeRating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("MustUpdatePassword")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(65535);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordResetProviderId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(255);
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(255);
|
||||||
|
|
||||||
|
b.Property<int>("SubtitleMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SyncPlayAccess")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(255);
|
||||||
|
|
||||||
|
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)
|
||||||
|
.WithOne("DisplayPreferences")
|
||||||
|
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "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");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1601
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddDisplayPreferences : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DisplayPreferences",
|
||||||
|
schema: "jellyfin",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<Guid>(nullable: false),
|
||||||
|
Client = table.Column<string>(maxLength: 32, nullable: false),
|
||||||
|
ShowSidebar = table.Column<bool>(nullable: false),
|
||||||
|
ShowBackdrop = table.Column<bool>(nullable: false),
|
||||||
|
ScrollDirection = table.Column<int>(nullable: false),
|
||||||
|
IndexBy = table.Column<int>(nullable: true),
|
||||||
|
SkipForwardLength = table.Column<int>(nullable: false),
|
||||||
|
SkipBackwardLength = table.Column<int>(nullable: false),
|
||||||
|
ChromecastVersion = table.Column<int>(nullable: false),
|
||||||
|
EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false),
|
||||||
|
DashboardTheme = table.Column<string>(maxLength: 32, nullable: true),
|
||||||
|
TvHome = table.Column<string>(maxLength: 32, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DisplayPreferences", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_DisplayPreferences_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalSchema: "jellyfin",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ItemDisplayPreferences",
|
||||||
|
schema: "jellyfin",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<Guid>(nullable: false),
|
||||||
|
ItemId = table.Column<Guid>(nullable: false),
|
||||||
|
Client = table.Column<string>(maxLength: 32, nullable: false),
|
||||||
|
ViewType = table.Column<int>(nullable: false),
|
||||||
|
RememberIndexing = table.Column<bool>(nullable: false),
|
||||||
|
IndexBy = table.Column<int>(nullable: true),
|
||||||
|
RememberSorting = table.Column<bool>(nullable: false),
|
||||||
|
SortBy = table.Column<string>(maxLength: 64, nullable: false),
|
||||||
|
SortOrder = table.Column<int>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ItemDisplayPreferences_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalSchema: "jellyfin",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "HomeSection",
|
||||||
|
schema: "jellyfin",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
DisplayPreferencesId = table.Column<int>(nullable: false),
|
||||||
|
Order = table.Column<int>(nullable: false),
|
||||||
|
Type = table.Column<int>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_HomeSection", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
|
||||||
|
column: x => x.DisplayPreferencesId,
|
||||||
|
principalSchema: "jellyfin",
|
||||||
|
principalTable: "DisplayPreferences",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DisplayPreferences_UserId",
|
||||||
|
schema: "jellyfin",
|
||||||
|
table: "DisplayPreferences",
|
||||||
|
column: "UserId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_HomeSection_DisplayPreferencesId",
|
||||||
|
schema: "jellyfin",
|
||||||
|
table: "HomeSection",
|
||||||
|
column: "DisplayPreferencesId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ItemDisplayPreferences_UserId",
|
||||||
|
schema: "jellyfin",
|
||||||
|
table: "ItemDisplayPreferences",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "HomeSection",
|
||||||
|
schema: "jellyfin");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ItemDisplayPreferences",
|
||||||
|
schema: "jellyfin");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DisplayPreferences",
|
||||||
|
schema: "jellyfin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("jellyfin")
|
.HasDefaultSchema("jellyfin")
|
||||||
.HasAnnotation("ProductVersion", "3.1.4");
|
.HasAnnotation("ProductVersion", "3.1.6");
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
{
|
{
|
||||||
|
@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
b.ToTable("ActivityLogs");
|
b.ToTable("ActivityLogs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<string>("DashboardTheme")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("IndexBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.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 =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
b.ToTable("ImageInfos");
|
b.ToTable("ImageInfos");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Client")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(32);
|
||||||
|
|
||||||
|
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()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(64);
|
||||||
|
|
||||||
|
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 =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithOne("DisplayPreferences")
|
||||||
|
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "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 =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
|
.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 =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
|
|
@ -4,13 +4,14 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Users
|
namespace Jellyfin.Server.Implementations.Users
|
||||||
|
@ -22,8 +23,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
{
|
{
|
||||||
private const string BaseResetFileName = "passwordreset";
|
private const string BaseResetFileName = "passwordreset";
|
||||||
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IApplicationHost _appHost;
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
|
|
||||||
private readonly string _passwordResetFileBase;
|
private readonly string _passwordResetFileBase;
|
||||||
private readonly string _passwordResetFileBaseDir;
|
private readonly string _passwordResetFileBaseDir;
|
||||||
|
@ -32,17 +32,13 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
/// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
|
/// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">The configuration manager.</param>
|
/// <param name="configurationManager">The configuration manager.</param>
|
||||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
/// <param name="appHost">The application host.</param>
|
||||||
/// <param name="userManager">The user manager.</param>
|
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost)
|
||||||
public DefaultPasswordResetProvider(
|
|
||||||
IServerConfigurationManager configurationManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IUserManager userManager)
|
|
||||||
{
|
{
|
||||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
|
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
|
||||||
_jsonSerializer = jsonSerializer;
|
_appHost = appHost;
|
||||||
_userManager = userManager;
|
// TODO: Remove the circular dependency on UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -54,13 +50,14 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||||
{
|
{
|
||||||
|
var userManager = _appHost.Resolve<IUserManager>();
|
||||||
var usersReset = new List<string>();
|
var usersReset = new List<string>();
|
||||||
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
|
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
|
||||||
{
|
{
|
||||||
SerializablePasswordReset spr;
|
SerializablePasswordReset spr;
|
||||||
await using (var str = File.OpenRead(resetFile))
|
await using (var str = File.OpenRead(resetFile))
|
||||||
{
|
{
|
||||||
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spr.ExpirationDate < DateTime.UtcNow)
|
if (spr.ExpirationDate < DateTime.UtcNow)
|
||||||
|
@ -72,10 +69,10 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
pin.Replace("-", string.Empty, StringComparison.Ordinal),
|
pin.Replace("-", string.Empty, StringComparison.Ordinal),
|
||||||
StringComparison.InvariantCultureIgnoreCase))
|
StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
var resetUser = _userManager.GetUserByName(spr.UserName)
|
var resetUser = userManager.GetUserByName(spr.UserName)
|
||||||
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
|
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
|
||||||
|
|
||||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||||
usersReset.Add(resetUser.Username);
|
usersReset.Add(resetUser.Username);
|
||||||
File.Delete(resetFile);
|
File.Delete(resetFile);
|
||||||
}
|
}
|
||||||
|
@ -116,12 +113,11 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
|
|
||||||
await using (FileStream fileStream = File.OpenWrite(filePath))
|
await using (FileStream fileStream = File.OpenWrite(filePath))
|
||||||
{
|
{
|
||||||
_jsonSerializer.SerializeToStream(spr, fileStream);
|
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
|
||||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.EasyPassword = pin;
|
user.EasyPassword = pin;
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new ForgotPasswordResult
|
return new ForgotPasswordResult
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
#pragma warning disable CA1307
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Users
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the storage and retrieval of display preferences through Entity Framework.
|
||||||
|
/// </summary>
|
||||||
|
public class DisplayPreferencesManager : IDisplayPreferencesManager
|
||||||
|
{
|
||||||
|
private readonly JellyfinDbProvider _dbProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbProvider">The Jellyfin db provider.</param>
|
||||||
|
public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
|
||||||
|
{
|
||||||
|
_dbProvider = dbProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
var prefs = dbContext.DisplayPreferences
|
||||||
|
.Include(pref => pref.HomeSections)
|
||||||
|
.FirstOrDefault(pref =>
|
||||||
|
pref.UserId == userId && string.Equals(pref.Client, client));
|
||||||
|
|
||||||
|
if (prefs == null)
|
||||||
|
{
|
||||||
|
prefs = new DisplayPreferences(userId, client);
|
||||||
|
dbContext.DisplayPreferences.Add(prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
var prefs = dbContext.ItemDisplayPreferences
|
||||||
|
.FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
|
||||||
|
|
||||||
|
if (prefs == null)
|
||||||
|
{
|
||||||
|
prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
|
||||||
|
dbContext.ItemDisplayPreferences.Add(prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
|
return dbContext.ItemDisplayPreferences
|
||||||
|
.Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SaveChanges(DisplayPreferences preferences)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
dbContext.Update(preferences);
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SaveChanges(ItemDisplayPreferences preferences)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
dbContext.Update(preferences);
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
private readonly IApplicationHost _appHost;
|
private readonly IApplicationHost _appHost;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly ILogger<UserManager> _logger;
|
private readonly ILogger<UserManager> _logger;
|
||||||
|
private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
|
||||||
private IAuthenticationProvider[] _authenticationProviders = null!;
|
private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
|
||||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
|
private readonly InvalidAuthProvider _invalidAuthProvider;
|
||||||
private InvalidAuthProvider _invalidAuthProvider = null!;
|
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||||
private IPasswordResetProvider[] _passwordResetProviders = null!;
|
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
||||||
|
@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
|
||||||
|
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
|
||||||
|
|
||||||
|
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
||||||
|
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||||
|
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
|
public IEnumerable<Guid> UsersIds
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
return dbContext.Users
|
||||||
|
.Select(user => user.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public User? GetUserById(Guid id)
|
public User? GetUserById(Guid id)
|
||||||
|
@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
|
||||||
|
{
|
||||||
|
// TODO: Remove after user item data is migrated.
|
||||||
|
var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
|
||||||
|
? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return new User(
|
||||||
|
name,
|
||||||
|
_defaultAuthenticationProvider.GetType().FullName,
|
||||||
|
_defaultPasswordResetProvider.GetType().FullName)
|
||||||
|
{
|
||||||
|
InternalId = max + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public User CreateUser(string name)
|
public async Task<User> CreateUserAsync(string name)
|
||||||
{
|
{
|
||||||
if (!IsValidUsername(name))
|
if (!IsValidUsername(name))
|
||||||
{
|
{
|
||||||
|
@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
// TODO: Remove after user item data is migrated.
|
var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
|
||||||
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
|
|
||||||
|
|
||||||
var newUser = new User(
|
|
||||||
name,
|
|
||||||
_defaultAuthenticationProvider.GetType().FullName,
|
|
||||||
_defaultPasswordResetProvider.GetType().FullName)
|
|
||||||
{
|
|
||||||
InternalId = max + 1
|
|
||||||
};
|
|
||||||
dbContext.Users.Add(newUser);
|
dbContext.Users.Add(newUser);
|
||||||
dbContext.SaveChanges();
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
|
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
|
||||||
|
|
||||||
|
@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
IncrementInvalidLoginAttemptCount(user);
|
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
||||||
user.Username,
|
user.Username,
|
||||||
|
@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
if (user != null && isInNetwork)
|
if (user != null && isInNetwork)
|
||||||
{
|
{
|
||||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||||
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
|
var result = await passwordResetProvider
|
||||||
|
.StartForgotPasswordProcess(user, isInNetwork)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ForgotPasswordResult
|
return new ForgotPasswordResult
|
||||||
|
@ -560,48 +588,32 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
|
|
||||||
{
|
|
||||||
_authenticationProviders = authenticationProviders.ToArray();
|
|
||||||
_passwordResetProviders = passwordResetProviders.ToArray();
|
|
||||||
|
|
||||||
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
|
||||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
|
||||||
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Initialize()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||||
using var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
if (dbContext.Users.Any())
|
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultName = Environment.UserName;
|
var defaultName = Environment.UserName;
|
||||||
if (string.IsNullOrWhiteSpace(defaultName))
|
if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
|
||||||
{
|
{
|
||||||
defaultName = "MyJellyfinUser";
|
defaultName = "MyJellyfinUser";
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
||||||
|
|
||||||
if (!IsValidUsername(defaultName))
|
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
||||||
{
|
|
||||||
throw new ArgumentException("Provided username is not valid!", defaultName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newUser = CreateUser(defaultName);
|
|
||||||
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
||||||
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
||||||
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
|
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
|
||||||
|
|
||||||
dbContext.Users.Update(newUser);
|
dbContext.Users.Add(newUser);
|
||||||
dbContext.SaveChanges();
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -637,7 +649,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void UpdateConfiguration(Guid userId, UserConfiguration config)
|
public void UpdateConfiguration(Guid userId, UserConfiguration config)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
var user = dbContext.Users
|
var user = dbContext.Users
|
||||||
.Include(u => u.Permissions)
|
.Include(u => u.Permissions)
|
||||||
.Include(u => u.Preferences)
|
.Include(u => u.Preferences)
|
||||||
|
@ -670,7 +682,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void UpdatePolicy(Guid userId, UserPolicy policy)
|
public void UpdatePolicy(Guid userId, UserPolicy policy)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
var user = dbContext.Users
|
var user = dbContext.Users
|
||||||
.Include(u => u.Permissions)
|
.Include(u => u.Permissions)
|
||||||
.Include(u => u.Preferences)
|
.Include(u => u.Preferences)
|
||||||
|
@ -749,8 +761,8 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
{
|
{
|
||||||
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||||
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
|
||||||
return Regex.IsMatch(name, @"^[\w\-'._@]*$");
|
return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
|
||||||
}
|
}
|
||||||
|
|
||||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||||
|
@ -882,7 +894,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncrementInvalidLoginAttemptCount(User user)
|
private async Task IncrementInvalidLoginAttemptCount(User user)
|
||||||
{
|
{
|
||||||
user.InvalidLoginAttemptCount++;
|
user.InvalidLoginAttemptCount++;
|
||||||
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
|
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
|
||||||
|
@ -896,7 +908,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||||
user.InvalidLoginAttemptCount);
|
user.InvalidLoginAttemptCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateUser(user);
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
|
||||||
using Jellyfin.Server.Implementations.Activity;
|
using Jellyfin.Server.Implementations.Activity;
|
||||||
using Jellyfin.Server.Implementations.Users;
|
using Jellyfin.Server.Implementations.Users;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
|
@ -33,9 +34,9 @@ namespace Jellyfin.Server
|
||||||
/// <param name="fileSystem">The <see cref="IFileSystem" /> 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="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
|
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
|
||||||
public CoreAppHost(
|
public CoreAppHost(
|
||||||
ServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
StartupOptions options,
|
IStartupOptions options,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
INetworkManager networkManager)
|
INetworkManager networkManager)
|
||||||
: base(
|
: base(
|
||||||
|
@ -63,16 +64,18 @@ namespace Jellyfin.Server
|
||||||
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
|
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Set up scoping and use AddDbContextPool
|
// TODO: Set up scoping and use AddDbContextPool,
|
||||||
serviceCollection.AddDbContext<JellyfinDb>(
|
// can't register as Transient since tracking transient in GC is funky
|
||||||
options => options
|
// serviceCollection.AddDbContext<JellyfinDb>(
|
||||||
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
// options => options
|
||||||
ServiceLifetime.Transient);
|
// .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
||||||
|
// ServiceLifetime.Transient);
|
||||||
|
|
||||||
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
||||||
serviceCollection.AddSingleton<IUserManager, UserManager>();
|
serviceCollection.AddSingleton<IUserManager, UserManager>();
|
||||||
|
serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
|
||||||
|
|
||||||
base.RegisterServices(serviceCollection);
|
base.RegisterServices(serviceCollection);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
|
||||||
<PackageReference Include="prometheus-net" Version="3.6.0" />
|
<PackageReference Include="prometheus-net" Version="3.6.0" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||||
|
|
|
@ -21,7 +21,9 @@ namespace Jellyfin.Server.Migrations
|
||||||
typeof(Routines.MigrateActivityLogDb),
|
typeof(Routines.MigrateActivityLogDb),
|
||||||
typeof(Routines.RemoveDuplicateExtras),
|
typeof(Routines.RemoveDuplicateExtras),
|
||||||
typeof(Routines.AddDefaultPluginRepository),
|
typeof(Routines.AddDefaultPluginRepository),
|
||||||
typeof(Routines.MigrateUserDb)
|
typeof(Routines.MigrateUserDb),
|
||||||
|
typeof(Routines.ReaddDefaultPluginRepository),
|
||||||
|
typeof(Routines.MigrateDisplayPreferencesDb)
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Server.Implementations;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The migration routine for migrating the display preferences database to EF Core.
|
||||||
|
/// </summary>
|
||||||
|
public class MigrateDisplayPreferencesDb : IMigrationRoutine
|
||||||
|
{
|
||||||
|
private const string DbFilename = "displaypreferences.db";
|
||||||
|
|
||||||
|
private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
|
||||||
|
private readonly IServerApplicationPaths _paths;
|
||||||
|
private readonly JellyfinDbProvider _provider;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="paths">The server application paths.</param>
|
||||||
|
/// <param name="provider">The database provider.</param>
|
||||||
|
public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_paths = paths;
|
||||||
|
_provider = provider;
|
||||||
|
_jsonOptions = new JsonSerializerOptions();
|
||||||
|
_jsonOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "MigrateDisplayPreferencesDatabase";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool PerformOnNewInstall => false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Perform()
|
||||||
|
{
|
||||||
|
HomeSectionType[] defaults =
|
||||||
|
{
|
||||||
|
HomeSectionType.SmallLibraryTiles,
|
||||||
|
HomeSectionType.Resume,
|
||||||
|
HomeSectionType.ResumeAudio,
|
||||||
|
HomeSectionType.LiveTv,
|
||||||
|
HomeSectionType.NextUp,
|
||||||
|
HomeSectionType.LatestMedia,
|
||||||
|
HomeSectionType.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "stable", ChromecastVersion.Stable },
|
||||||
|
{ "nightly", ChromecastVersion.Unstable },
|
||||||
|
{ "unstable", ChromecastVersion.Unstable }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||||
|
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
|
||||||
|
{
|
||||||
|
using var dbContext = _provider.CreateContext();
|
||||||
|
|
||||||
|
var results = connection.Query("SELECT * FROM userdisplaypreferences");
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
|
||||||
|
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
|
||||||
|
? chromecastDict[version]
|
||||||
|
: ChromecastVersion.Stable;
|
||||||
|
|
||||||
|
var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
|
||||||
|
{
|
||||||
|
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
|
||||||
|
ShowBackdrop = dto.ShowBackdrop,
|
||||||
|
ShowSidebar = dto.ShowSidebar,
|
||||||
|
ScrollDirection = dto.ScrollDirection,
|
||||||
|
ChromecastVersion = chromecastVersion,
|
||||||
|
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
|
||||||
|
? int.Parse(length, CultureInfo.InvariantCulture)
|
||||||
|
: 30000,
|
||||||
|
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
|
||||||
|
? int.Parse(length, CultureInfo.InvariantCulture)
|
||||||
|
: 10000,
|
||||||
|
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
|
||||||
|
? bool.Parse(enabled)
|
||||||
|
: true,
|
||||||
|
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
|
||||||
|
TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
|
||||||
|
|
||||||
|
displayPreferences.HomeSections.Add(new HomeSection
|
||||||
|
{
|
||||||
|
Order = i,
|
||||||
|
Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
|
||||||
|
{
|
||||||
|
SortBy = dto.SortBy ?? "SortName",
|
||||||
|
SortOrder = dto.SortOrder,
|
||||||
|
RememberIndexing = dto.RememberIndexing,
|
||||||
|
RememberSorting = dto.RememberSorting,
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Add(defaultLibraryPrefs);
|
||||||
|
|
||||||
|
foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
|
||||||
|
{
|
||||||
|
SortBy = dto.SortBy ?? "SortName",
|
||||||
|
SortOrder = dto.SortOrder,
|
||||||
|
RememberIndexing = dto.RememberIndexing,
|
||||||
|
RememberSorting = dto.RememberSorting,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType))
|
||||||
|
{
|
||||||
|
libraryDisplayPreferences.ViewType = viewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.Add(displayPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(dbFilePath, dbFilePath + ".old");
|
||||||
|
|
||||||
|
var journalPath = dbFilePath + "-journal";
|
||||||
|
if (File.Exists(journalPath))
|
||||||
|
{
|
||||||
|
File.Move(journalPath, dbFilePath + ".old-journal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Model.Updates;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Migration to initialize system configuration with the default plugin repository.
|
||||||
|
/// </summary>
|
||||||
|
public class ReaddDefaultPluginRepository : IMigrationRoutine
|
||||||
|
{
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
|
private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
|
||||||
|
{
|
||||||
|
Name = "Jellyfin Stable",
|
||||||
|
Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "ReaddDefaultPluginRepository";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool PerformOnNewInstall => true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Perform()
|
||||||
|
{
|
||||||
|
// Only add if repository list is empty
|
||||||
|
if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0)
|
||||||
|
{
|
||||||
|
_serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
|
||||||
|
_serverConfigurationManager.SaveConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -343,6 +343,21 @@ namespace Jellyfin.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind to unix socket (only on OSX and Linux)
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// TODO: allow configuration of socket path
|
||||||
|
var socketPath = $"{appPaths.DataPath}/socket.sock";
|
||||||
|
// Workaround for https://github.com/aspnet/AspNetCore/issues/14134
|
||||||
|
if (File.Exists(socketPath))
|
||||||
|
{
|
||||||
|
File.Delete(socketPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.ListenUnixSocket(socketPath);
|
||||||
|
_logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
|
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
|
||||||
.UseSerilog()
|
.UseSerilog()
|
||||||
|
|
|
@ -194,7 +194,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
|
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
|
||||||
|
|
||||||
// Main stream
|
// Main stream
|
||||||
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
|
builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
|
||||||
|
.AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
|
||||||
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
|
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
|
||||||
builder.AppendLine(playlistUrl);
|
builder.AppendLine(playlistUrl);
|
||||||
|
|
||||||
|
|
|
@ -361,7 +361,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
|
|
||||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
||||||
|
|
||||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
var indexString = Path.GetFileNameWithoutExtension(file.Name).AsSpan().Slice(playlistFilename.Length);
|
||||||
|
|
||||||
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
@ -960,7 +960,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
builder.AppendLine("#EXTM3U");
|
builder.AppendLine("#EXTM3U");
|
||||||
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
||||||
builder.AppendLine("#EXT-X-VERSION:3");
|
builder.AppendLine("#EXT-X-VERSION:3");
|
||||||
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
|
builder.Append("#EXT-X-TARGETDURATION:")
|
||||||
|
.AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
|
||||||
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
|
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
|
||||||
|
|
||||||
var queryStringIndex = Request.RawUrl.IndexOf('?');
|
var queryStringIndex = Request.RawUrl.IndexOf('?');
|
||||||
|
@ -975,14 +976,17 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
|
|
||||||
foreach (var length in segmentLengths)
|
foreach (var length in segmentLengths)
|
||||||
{
|
{
|
||||||
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
|
builder.Append("#EXTINF:")
|
||||||
|
.Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
|
||||||
builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
|
.AppendLine(", nodesc");
|
||||||
|
|
||||||
|
builder.AppendFormat(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"hls1/{0}/{1}{2}{3}",
|
||||||
name,
|
name,
|
||||||
index.ToString(CultureInfo.InvariantCulture),
|
index.ToString(CultureInfo.InvariantCulture),
|
||||||
GetSegmentFileExtension(request),
|
GetSegmentFileExtension(request),
|
||||||
queryString));
|
queryString).AppendLine();
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user