Merge pull request #3808 from crobibero/api-migration-merge

Merge master into api-migration
This commit is contained in:
David 2020-08-03 20:22:21 +02:00 committed by GitHub
commit a28d00eeba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 3459 additions and 1959 deletions

View File

@ -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'

View File

@ -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;

View File

@ -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,

View File

@ -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; }

View File

@ -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>");

View File

@ -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)

View File

@ -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[]
{
"<",
"&lt;",
">",
"&gt;",
"\"",
"&quot;",
"'",
"&apos;",
"&",
"&amp;"
};
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()

View File

@ -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>");

View File

@ -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 />

View File

@ -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

View File

@ -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))
{ {

View File

@ -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>());
} }

View File

@ -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;
} }

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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
{ {

View File

@ -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-->

View File

@ -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;

View File

@ -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
{ {

View File

@ -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;

View File

@ -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);
} }
} }
} }

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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()
{ {

View File

@ -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",

View File

@ -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)

View File

@ -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)

View File

@ -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();
} }
} }
} }

View File

@ -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)
{ {

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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 + "\",";
} }

View File

@ -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));

View File

@ -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;

View File

@ -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);

View File

@ -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[] { ' ', '-' });

View File

@ -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.",

View File

@ -92,7 +92,7 @@
"HeaderRecordingGroups": "錄製組", "HeaderRecordingGroups": "錄製組",
"Inherit": "繼承", "Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。", "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道", "TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新插件", "TaskUpdatePlugins": "更新插件",

View File

@ -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)

View File

@ -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();

View File

@ -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)
{ {

View File

@ -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("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
}
private static string Escape(string content)
{
if (content == null)
{
return null;
}
return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
}
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));
} }
} }
} }

View File

@ -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>();
} }

View File

@ -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;

View File

@ -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());
} }
} }
} }

View File

@ -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)

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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)
{ {

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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();
} }

View File

@ -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;

View File

@ -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
{ {

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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)

View File

@ -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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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.

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View File

@ -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>

View File

@ -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);
}
} }
} }
} }

View File

@ -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);
}
} }
} }
} }

View File

@ -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;
} }

View File

@ -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);
}
}
}
} }
} }

View File

@ -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()
{ {
} }

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }
} }

View File

@ -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)
{ {

View File

@ -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);
} }
} }
} }

View 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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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)

View File

@ -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
{ {

View File

@ -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();
}
}
}

View File

@ -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);
} }
} }
} }

View File

@ -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);
} }

View File

@ -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" />

View File

@ -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>

View File

@ -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'");
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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()

View File

@ -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);

View File

@ -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