aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml10
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs1
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs3
-rw-r--r--Emby.Dlna/DlnaManager.cs24
-rw-r--r--Emby.Dlna/Eventing/EventManager.cs14
-rw-r--r--Emby.Dlna/PlayTo/Device.cs4
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs182
-rw-r--r--Emby.Dlna/Service/ServiceXmlBuilder.cs34
-rw-r--r--Emby.Drawing/ImageProcessor.cs14
-rw-r--r--Emby.Naming/Common/NamingOptions.cs60
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs8
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs24
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs225
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs193
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs23
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs4
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs3
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs10
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs24
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs23
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs238
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs24
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs48
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs20
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs25
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs8
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs43
-rw-r--r--Emby.Server.Implementations/Networking/NetworkManager.cs4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs64
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs7
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs8
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs19
-rw-r--r--Emby.Server.Implementations/Services/ServiceExec.cs9
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs26
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs8
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs4
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs30
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs4
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs60
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs133
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs1
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs8
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs15
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs2
-rw-r--r--Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs106
-rw-r--r--Jellyfin.Data/Entities/DisplayPreferences.cs150
-rw-r--r--Jellyfin.Data/Entities/HomeSection.cs46
-rw-r--r--Jellyfin.Data/Entities/ItemDisplayPreferences.cs120
-rw-r--r--Jellyfin.Data/Entities/User.cs15
-rw-r--r--Jellyfin.Data/Enums/ChromecastVersion.cs18
-rw-r--r--Jellyfin.Data/Enums/HomeSectionType.cs53
-rw-r--r--Jellyfin.Data/Enums/IndexingKind.cs20
-rw-r--r--Jellyfin.Data/Enums/ScrollDirection.cs18
-rw-r--r--Jellyfin.Data/Enums/SortOrder.cs18
-rw-r--r--Jellyfin.Data/Enums/ViewType.cs38
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj9
-rw-r--r--Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs22
-rw-r--r--Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs34
-rw-r--r--Jellyfin.Drawing.Skia/SkiaCodecException.cs2
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs525
-rw-r--r--Jellyfin.Drawing.Skia/SkiaException.cs2
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs118
-rw-r--r--Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs52
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs17
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs14
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs459
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs132
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs149
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs28
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs88
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs108
-rw-r--r--Jellyfin.Server/CoreAppHost.cs17
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs174
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs49
-rw-r--r--Jellyfin.Server/Program.cs15
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs3
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs16
-rw-r--r--MediaBrowser.Common/Extensions/HttpContextExtensions.cs33
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs16
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs1
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs49
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs3
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs130
-rw-r--r--MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs53
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs11
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs5
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs11
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs36
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs28
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs9
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs2
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs2
-rw-r--r--MediaBrowser.Model/Entities/DisplayPreferencesDto.cs (renamed from MediaBrowser.Model/Entities/DisplayPreferences.cs)12
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs219
-rw-r--r--MediaBrowser.Model/Entities/ScrollDirection.cs18
-rw-r--r--MediaBrowser.Model/Entities/SortOrder.cs18
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs2
-rw-r--r--MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs2
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOption.cs13
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs4
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj2
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html42
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html62
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html35
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs4
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs22
-rw-r--r--README.md3
-rw-r--r--SharedVersion.cs4
-rw-r--r--build.yaml2
-rwxr-xr-xbump_version3
-rw-r--r--debian/changelog6
-rw-r--r--debian/conf/jellyfin4
-rw-r--r--debian/metapackage/jellyfin2
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs14
153 files changed, 3426 insertions, 1926 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 272111299..1677f71c7 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -80,7 +80,15 @@ jobs:
pool:
vmImage: 'ubuntu-latest'
+ variables:
+ - name: JellyfinVersion
+ value: 0.0.0
+
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
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -105,7 +113,7 @@ jobs:
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration)
- stable-$(BuildConfiguration)
+ $(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
displayName: 'Collect Artifacts'
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 291de5245..00821bf78 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -11,6 +11,7 @@ using System.Xml;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 66baa9512..70e358019 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
}
- var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
+ var mediaProfile = _profile.GetVideoMediaProfile(
+ streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 5911a73ef..269f7ee43 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -122,15 +122,15 @@ namespace Emby.Dlna
var builder = new StringBuilder();
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.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
- builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
- builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
- builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
- builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
- builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
- builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
- builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
+ builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
+ builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
_logger.LogInformation(builder.ToString());
}
@@ -387,7 +387,7 @@ namespace Emby.Dlna
foreach (var name in _assembly.GetManifestResourceNames())
{
- if (!name.StartsWith(namespaceName))
+ if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
{
continue;
}
@@ -406,7 +406,7 @@ namespace Emby.Dlna
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);
}
- class InternalProfileInfo
+ private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs
index 56c90c8b3..7d02f5e96 100644
--- a/Emby.Dlna/Eventing/EventManager.cs
+++ b/Emby.Dlna/Eventing/EventManager.cs
@@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
foreach (var key in stateVariables.Keys)
{
- builder.Append("<e:property>");
- builder.Append("<" + key + ">");
- builder.Append(stateVariables[key]);
- builder.Append("</" + key + ">");
- builder.Append("</e:property>");
+ builder.Append("<e:property>")
+ .Append('<')
+ .Append(key)
+ .Append('>')
+ .Append(stateVariables[key])
+ .Append("</")
+ .Append(key)
+ .Append('>')
+ .Append("</e:property>");
}
builder.Append("</e:propertyset>");
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index c5080b90f..72834c69d 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Emby.Dlna.Common;
-using Emby.Dlna.Server;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
return string.Empty;
}
- return DescriptionXmlBuilder.Escape(value);
+ return SecurityElement.Escape(value);
}
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 7143c3109..bca9e81cd 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Security;
using System.Text;
using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
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("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
if (!EnableAbsoluteUrls)
{
- builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+ builder.Append("<URLBase>")
+ .Append(SecurityElement.Escape(_serverAddress))
+ .Append("</URLBase>");
}
AppendDeviceInfo(builder);
@@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
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);
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)
{
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("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
- builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
- builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
-
- builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
- builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
-
- builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
- builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+ builder.Append("<friendlyName>")
+ .Append(SecurityElement.Escape(GetFriendlyName()))
+ .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>")
+ .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
+ .Append("</modelDescription>");
+ builder.Append("<modelName>")
+ .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
+ .Append("</modelName>");
+
+ builder.Append("<modelNumber>")
+ .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))
{
- builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_serverId))
+ .Append("</serialNumber>");
}
else
{
- builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+ builder.Append("<serialNumber>")
+ .Append(SecurityElement.Escape(_profile.SerialNumber))
+ .Append("</serialNumber>");
}
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))
{
- 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("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
- builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
- builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
- builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
- builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
+ builder.Append("<mimetype>")
+ .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+ .Append("</mimetype>");
+ builder.Append("<width>")
+ .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>");
}
@@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
{
builder.Append("<service>");
- builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
- builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
- builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
- builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
- builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
+ builder.Append("<serviceType>")
+ .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+ .Append("</serviceType>");
+ builder.Append("<serviceId>")
+ .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>");
}
@@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + url;
}
- return Escape(url);
+ return SecurityElement.Escape(url);
}
private IEnumerable<DeviceIcon> GetIcons()
diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs
index af557aa14..6c7d6f846 100644
--- a/Emby.Dlna/Service/ServiceXmlBuilder.cs
+++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs
@@ -1,9 +1,9 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Security;
using System.Text;
using Emby.Dlna.Common;
-using Emby.Dlna.Server;
namespace Emby.Dlna.Service
{
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
{
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>");
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
{
builder.Append("<argument>");
- builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
- builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
- builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
+ builder.Append("<name>")
+ .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
+ .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>");
}
@@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
{
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("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
+ builder.Append("<name>")
+ .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)
{
builder.Append("<allowedValueList>");
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>");
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8696cb280..f585b90ca 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -448,21 +448,21 @@ namespace Emby.Drawing
/// or
/// filename.
/// </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 />
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 1b343790e..d1e17f416 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
CleanDateTimes = new[]
{
- @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{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})*",
+ @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
};
CleanStrings = new[]
@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first.
// "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
},
@@ -300,32 +300,32 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
// [bar] Foo - 1 [baz]
- new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+ new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
- new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+ new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{
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
},
- 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
},
- 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
},
// "01.avi"
- new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+ new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
{
IsOptimistic = true,
IsNamed = true
@@ -335,34 +335,34 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
// "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,
IsNamed = true
},
// "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,
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"
- new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+ new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
},
// "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,
IsNamed = true
},
// "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,
IsNamed = true
@@ -625,17 +625,17 @@ namespace Emby.Naming.Common
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
- @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+ @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// 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>\d+)",
+ "^(?<chapter>[0-9]+)",
// Part if often ending of filename
- @"(?<part>\d+)$",
+ "(?<part>[0-9]+)$",
// 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.
- @"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
+ @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
var extensions = VideoFileExtensions.ToList();
@@ -675,16 +675,16 @@ namespace Emby.Naming.Common
MultipleEpisodeExpressions = new string[]
{
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{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]?\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]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{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](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
- @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<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>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+ @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{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]?[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]?[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]?[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>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{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)
{
IsNamed = true
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index 2fa6b4353..d2e324dda 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -77,7 +77,7 @@ namespace Emby.Naming.TV
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))
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index ad6cbe167..7647827fb 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
- protected ServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets all concrete types.
@@ -235,7 +235,7 @@ namespace Emby.Server.Implementations
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary>
public ApplicationHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
@@ -553,8 +553,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
-
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@@ -651,7 +649,6 @@ namespace Emby.Server.Implementations
_httpServer = Resolve<IHttpServer>();
_httpClient = Resolve<IHttpClient>();
- ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
@@ -796,7 +793,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
- Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index c803d9d82..8d6292867 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -7,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
-
- private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
- new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
+ private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
/// <summary>
@@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
+ /// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IMemoryCache memoryCache)
{
_userManager = userManager;
_dtoService = dtoService;
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
+ _memoryCache = memoryCache;
}
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)
{
- 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.Item2;
- }
+ return cachedInfo;
}
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false);
var list = mediaInfo.ToList();
-
- var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
- _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+ _memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
return list;
}
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
deleted file mode 100644
index 5597155a8..000000000
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a6390b1ef..1c6d3cb94 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
@@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
"OwnerId"
};
+ private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
@@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
"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 =
{
"ItemId",
@@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
"MIMEType"
};
- private static readonly string _mediaAttachmentInsertPrefix;
-
- private static string GetSaveItemCommandText()
- {
- 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"
- };
-
- var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
-
- for (var i = 0; i < saveColumns.Length; i++)
- {
- if (i != 0)
- {
- saveItemCommandCommandText += ",";
- }
+ private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
- saveItemCommandCommandText += "@" + saveColumns[i];
- }
+ private static readonly string _mediaAttachmentInsertPrefix;
- return saveItemCommandCommandText + ")";
- }
+ private const string SaveItemCommandText =
+ @"replace into TypedBaseItems
+ (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)";
/// <summary>
/// Save a standard item in the repo.
@@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
{
var statements = PrepareAll(db, new string[]
{
- GetSaveItemCommandText(),
+ SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
}).ToList();
@@ -1110,7 +1032,8 @@ namespace Emby.Server.Implementations.Data
continue;
}
- str.Append(ToValueString(i) + "|");
+ str.Append(ToValueString(i))
+ .Append('|');
}
str.Length -= 1; // Remove last |
@@ -1225,7 +1148,7 @@ namespace Emby.Server.Implementations.Data
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);
@@ -2471,7 +2394,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo;
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating))
{
@@ -2509,7 +2432,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm))
{
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
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)
{
- if (buffer.IndexOf('\u2013') > -1)
+ if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2013', '-'); // en dash
}
- if (buffer.IndexOf('\u2014') > -1)
+ if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2014', '-'); // em dash
}
- if (buffer.IndexOf('\u2015') > -1)
+ if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
{
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
}
- if (buffer.IndexOf('\u2018') > -1)
+ if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
{
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
}
- if (buffer.IndexOf('\u201a') > -1)
+ if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
{
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
}
- if (buffer.IndexOf('\u201c') > -1)
+ if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
{
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
}
- if (buffer.IndexOf('\u201e') > -1)
+ if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
{
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
}
- if (buffer.IndexOf('\u2033') > -1)
+ if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u2033', '\"'); // double prime
}
- if (buffer.IndexOf('\u0060') > -1)
+ if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u0060', '\''); // grave accent
}
- if (buffer.IndexOf('\u00B4') > -1)
+ if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
{
buffer = buffer.Replace('\u00B4', '\''); // acute accent
}
@@ -2999,7 +2922,6 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
-
var statements = PrepareAll(db, statementTexts).ToList();
if (!isReturningZeroItems)
@@ -4669,8 +4591,12 @@ namespace Emby.Server.Implementations.Data
if (query.BlockUnratedItems.Length > 1)
{
- var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
- whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
+ var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
+ whereClauses.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
+ inClause));
}
if (query.ExcludeInheritedTags.Length > 0)
@@ -4679,7 +4605,7 @@ namespace Emby.Server.Implementations.Data
if (statement == null)
{
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)");
}
else
@@ -5238,7 +5164,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
if (i > 0)
{
- insertText.Append(",");
+ insertText.Append(',');
}
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));
}
- var cmdText = "select "
- + string.Join(",", _mediaStreamSaveColumns)
- + " from mediastreams where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaStreamSaveColumnsSelectQuery;
if (query.Type.HasValue)
{
@@ -5972,15 +5895,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
while (startIndex < streams.Count)
{
- var insertText = new StringBuilder("insert into mediastreams (");
- foreach (var column in _mediaStreamSaveColumns)
- {
- insertText.Append(column).Append(',');
- }
-
- // Remove last comma
- insertText.Length--;
- insertText.Append(") values ");
+ var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
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));
}
- var cmdText = "select "
- + string.Join(",", _mediaAttachmentSaveColumns)
- + " from mediaattachments where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
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))
{
- insertText.Append("@" + column + index + ",");
+ insertText.Append('@')
+ .Append(column)
+ .Append(index)
+ .Append(',');
}
insertText.Length -= 1;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index e75745cc6..2921a7f0e 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using Jellyfin.Data.Enums;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
+ private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo;
- private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
private readonly object _capabilitiesSyncLock = new object();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IMemoryCache memoryCache)
{
_json = json;
_userManager = userManager;
_config = config;
+ _memoryCache = memoryCache;
_authRepo = authRepo;
- _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock)
{
- _capabilitiesCache[deviceId] = capabilities;
-
+ _memoryCache.CreateEntry(deviceId).SetValue(capabilities);
_json.SerializeToFile(capabilities, path);
}
}
@@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id)
{
- lock (_capabilitiesSyncLock)
+ if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
{
- if (_capabilitiesCache.TryGetValue(id, out var result))
- {
- return result;
- }
+ return result;
+ }
+ lock (_capabilitiesSyncLock)
+ {
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index adae920dc..a1103b051 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -24,7 +24,7 @@
<ItemGroup>
<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.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.Configuration.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="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="DotNet.Glob" Version="3.0.9" />
</ItemGroup>
@@ -53,7 +53,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
+ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
index 590eee1b4..6fce8de44 100644
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs
@@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
private readonly IStreamHelper _streamHelper;
private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
/// <summary>
/// The _options.
@@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
}
_streamHelper = streamHelper;
- _fileSystem = fileSystem;
Path = path;
_logger = logger;
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index c3428ee62..0d4a789b5 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+ foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
{
httpRes.Headers.Add(key, value);
}
@@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
var handler = GetServiceHandler(httpReq);
if (handler != null)
{
- await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
+ await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
}
else
{
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 6a2d8fdbb..76c1d9bac 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer.Security
{
public class AuthService : IAuthService
{
- private readonly ILogger<AuthService> _logger;
private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
public AuthService(
- ILogger<AuthService> logger,
IAuthorizationContext authorizationContext,
IServerConfigurationManager config,
ISessionManager sessionManager,
INetworkManager networkManager)
{
- _logger = logger;
_authorizationContext = authorizationContext;
_config = config;
_sessionManager = sessionManager;
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index ef93779aa..fe74f1de7 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
private Timer _timer;
+ private bool _disposed;
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()
{
_disposed = true;
DisposeTimer();
+ GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index 52896720e..bf57382ed 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
/// </summary>
public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
{
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
- _libraryManager = libraryManager;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index da88b8d8a..161b4c452 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index e9523386e..0224ab32a 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index d2aeccdb2..1cd4cd66b 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 77b2c0a69..3380e29d4 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// 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))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index ab39a7223..236453e80 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
{
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 string OriginalStreamId { get; set; }
@@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
public MediaSourceInfo MediaSource { get; set; }
- public string UniqueId { get; private set; }
-
- 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 string UniqueId { get; }
public Task Close()
{
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 6e6ef1359..e30a67593 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
{
"**/small.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
"**/metadata/**",
@@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
"**/.grab/**",
"**/.grab",
- // Unix hidden files and directories
- "**/.*/**",
+ // Unix hidden files
"**/.*",
+ // Mac - if you ever remove the above.
+ // "**/._*",
+ // "**/.DS_Store",
+
// thumbs.db
"**/thumbs.db",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index c27b73c74..04b530fce 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
using VideoResolver = Emby.Naming.Video.VideoResolver;
namespace Emby.Server.Implementations.Library
@@ -60,7 +59,10 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class LibraryManager : ILibraryManager
{
+ private const string ShortcutFileExtension = ".mblink";
+
private readonly ILogger<LibraryManager> _logger;
+ private readonly IMemoryCache _memoryCache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -72,66 +74,26 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
- private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor;
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
- private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
- private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
- /// <summary>
- /// Gets or sets the postscan tasks.
- /// </summary>
- /// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
- /// <summary>
- /// Gets or sets the intro providers.
- /// </summary>
- /// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; }
-
- /// <summary>
- /// Gets or sets the list of entity resolution ignore rules.
- /// </summary>
- /// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
- /// <summary>
- /// Gets or sets the list of currently registered entity resolvers.
- /// </summary>
- /// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; }
-
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
/// <summary>
- /// Gets or sets the comparers.
+ /// The _root folder sync lock.
/// </summary>
- /// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; }
+ private readonly object _rootFolderSyncLock = new object();
+ private readonly object _userRootFolderSyncLock = new object();
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemAdded;
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ private NamingOptions _namingOptions;
+ private string[] _videoFileExtensions;
/// <summary>
- /// Occurs when [item removed].
+ /// The _root folder.
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ private volatile AggregateFolder _rootFolder;
+ private volatile UserRootFolder _userRootFolder;
- public bool IsScanRunning { get; private set; }
+ private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -149,6 +111,7 @@ namespace Emby.Server.Implementations.Library
/// <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,
@@ -162,7 +125,8 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
- IImageProcessor imageProcessor)
+ IImageProcessor imageProcessor,
+ IMemoryCache memoryCache)
{
_appHost = appHost;
_logger = logger;
@@ -177,8 +141,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
-
- _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+ _memoryCache = memoryCache;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Adds the parts.
+ /// Occurs when [item added].
/// </summary>
- /// <param name="rules">The rules.</param>
- /// <param name="resolvers">The resolvers.</param>
- /// <param name="introProviders">The intro providers.</param>
- /// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
- public void AddParts(
- IEnumerable<IResolverIgnoreRule> rules,
- IEnumerable<IItemResolver> resolvers,
- IEnumerable<IIntroProvider> introProviders,
- IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
- {
- EntityResolutionIgnoreRules = rules.ToArray();
- EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
- MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
- IntroProviders = introProviders.ToArray();
- Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
- }
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
- /// The _root folder.
+ /// Occurs when [item updated].
/// </summary>
- private volatile AggregateFolder _rootFolder;
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
- /// The _root folder sync lock.
+ /// Occurs when [item removed].
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
}
}
- private bool _wizardCompleted;
+ private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+ private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+ private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of entity resolution ignore rules.
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of currently registered entity resolvers.
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The post scan tasks.</param>
+ public void AddParts(
+ IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+ PostscanTasks = postscanTasks.ToArray();
+ }
/// <summary>
/// Records the configuration values.
@@ -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)
@@ -441,7 +447,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
- _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
+ _memoryCache.Remove(item.Id);
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
key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
- .TrimStart(new[] { '/', '\\' })
- .Replace("/", "\\");
+ .TrimStart('/', '\\')
+ .Replace('/', '\\');
}
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -775,14 +781,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder;
}
- private volatile UserRootFolder _userRootFolder;
- private readonly object _syncLock = new object();
-
public Folder GetUserRootFolder()
{
if (_userRootFolder == null)
{
- lock (_syncLock)
+ lock (_userRootFolderSyncLock)
{
if (_userRootFolder == null)
{
@@ -1245,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
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;
}
@@ -1332,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
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);
}
- var list = _itemRepository.GetItemList(query);
-
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)
{
var tasks = IntroProviders
- .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1)
.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();
- if (outdated.Length == 0)
+ // Skip image processing if current or live tv source
+ if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{
RegisterItem(item);
return;
@@ -1945,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Updates the item.
/// </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
- var itemsList = items.ToList();
-
- foreach (var item in itemsList)
+ foreach (var item in items)
{
if (item.IsFileProtocol)
{
@@ -1962,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
}
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
@@ -2189,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
- private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
public UserView GetNamedView(
User user,
string name,
@@ -2488,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- var episodeInfo = episode.IsFileProtocol ?
- resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
- new Naming.TV.EpisodeInfo();
-
- if (episodeInfo == null)
- {
- episodeInfo = new Naming.TV.EpisodeInfo();
- }
+ var episodeInfo = episode.IsFileProtocol
+ ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+ : new Naming.TV.EpisodeInfo();
try
{
@@ -2503,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
// Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- }, CancellationToken.None).GetAwaiter().GetResult();
+ var mediaInfo = _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = episode.GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0)
{
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2665,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
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)
{
@@ -2682,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
.Select(video =>
{
// 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 (dbItem != null)
+ if (GetItemById(video.Id) is Trailer dbItem)
{
video = dbItem;
}
@@ -3011,8 +3002,6 @@ namespace Emby.Server.Implementations.Library
});
}
- private const string ShortcutFileExtension = ".mblink";
-
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3206,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
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)
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 9b9f53049..041619d1e 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
-
- private IJsonSerializer _json;
- private IApplicationPaths _appPaths;
+ private readonly IJsonSerializer _json;
+ private readonly IApplicationPaths _appPaths;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
{
@@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
- mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
+ mediaInfo = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+ },
+ cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
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)
{
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
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.BitRate.HasValue)
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index ceb36b389..67cf8bf5b 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
{
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 IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
@@ -40,6 +43,9 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
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;
public MediaSourceManager(
@@ -368,7 +374,6 @@ namespace Emby.Server.Implementations.Library
}
}
-
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
@@ -451,9 +456,6 @@ namespace Emby.Server.Implementations.Library
.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)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -619,12 +621,14 @@ namespace Emby.Server.Implementations.Library
if (liveStreamInfo is IDirectStreamProvider)
{
- var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- ExtractChapters = false,
- MediaType = DlnaProfileType.Video
- }, cancellationToken).ConfigureAwait(false);
+ var info = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ ExtractChapters = false,
+ MediaType = DlnaProfileType.Video
+ },
+ cancellationToken).ConfigureAwait(false);
mediaSource.MediaStreams = info.MediaStreams;
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 const char LiveStreamIdDelimeter = '_';
-
- private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+ private (IMediaSourceProvider, string) GetProvider(string 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 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);
- return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+ return (provider, keyId);
}
/// <summary>
@@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
- private readonly object _disposeLock = new object();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -892,15 +893,12 @@ namespace Emby.Server.Implementations.Library
{
if (dispose)
{
- lock (_disposeLock)
+ foreach (var key in _openStreams.Keys.ToList())
{
- foreach (var key in _openStreams.Keys.ToList())
- {
- var task = CloseLiveStream(key);
-
- Task.WaitAll(task);
- }
+ CloseLiveStream(key).GetAwaiter().GetResult();
}
+
+ _liveStreamSemaphore.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ca904c4ec..179e0ed98 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
}
// 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)
{
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 0bdc59914..877fdec86 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index c8e41001a..3332e1806 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,6 +1,5 @@
using System.Globalization;
using Emby.Naming.TV;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
@@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class SeasonResolver : FolderResolver<Season>
{
- private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly ILogger<SeasonResolver> _logger;
@@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary>
- /// <param name="config">The config.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="localization">The localization.</param>
/// <param name="logger">The logger.</param>
public SeasonResolver(
- IServerConfigurationManager config,
ILibraryManager libraryManager,
ILocalizationManager localization,
ILogger<SeasonResolver> logger)
{
- _config = config;
_libraryManager = libraryManager;
_localization = localization;
_logger = logger;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 3df9cc06f..9a69bce0e 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
@@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
{
public class SearchEngine : ISearchEngine
{
- private readonly ILogger<SearchEngine> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
+ public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
{
- _logger = logger;
_libraryManager = libraryManager;
_userManager = userManager;
}
@@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
User user = null;
-
- if (query.UserId.Equals(Guid.Empty))
- {
- }
- else
+ if (query.UserId != Guid.Empty)
{
user = _userManager.GetUserById(query.UserId);
}
@@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
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)
{
- results = results.Take(query.Limit.Value).ToList();
+ results = results.GetRange(0, query.Limit.Value);
}
return new QueryResult<SearchHintInfo>
{
TotalRecordCount = totalRecordCount,
- Items = results.ToArray()
+ Items = results
};
}
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
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();
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 175b3af57..f9e5e6bbc 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
@@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
- private readonly ILogger<UserDataManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
public UserDataManager(
- ILogger<UserDataManager> logger,
IServerConfigurationManager config,
IUserManager userManager,
IUserDataRepository repository)
{
- _logger = logger;
_config = config;
_userManager = userManager;
_repository = repository;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 7b0fcbc9e..80e09f0a3 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 3709f8fe4..77a7069eb 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
ListingsProviderInfo info,
List<string> programIds,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
@@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var imageId = i.Substring(0, 10);
- if (!imageIdString.Contains(imageId))
+ if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
{
imageIdString += "\"" + imageId + "\",";
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 4c1de3bcc..1b075d86a 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
@@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IChannelManager channelManager,
LiveTvDtoService liveTvDtoService)
@@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
@@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
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));
@@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
{
// Hack to make the object a pure ListingsProviderInfo instead of an 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));
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 2e2488e6e..00420bd2a 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
while (!sr.EndOfStream)
{
string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel"))
+ if (line.Contains("Channel", StringComparison.Ordinal))
{
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
@@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static string StripXML(string source)
{
+ if (string.IsNullOrEmpty(source))
+ {
+ return string.Empty;
+ }
+
char[] buffer = new char[source.Length];
int bufferIndex = 0;
bool inside = false;
@@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
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 isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 57c5b7500..d4a88e299 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -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;
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
+
+ GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
_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);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.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;
}
- var commandList = commands.GetCommands();
- foreach (var command in commandList)
+ foreach (var command in commands.GetCommands())
{
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
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);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index c798c0a85..875977219 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{
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 attributeValue;
- double doubleValue;
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;
}
@@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
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;
}
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;
}
}
}
- if (String.IsNullOrWhiteSpace(numberString))
+ if (string.IsNullOrWhiteSpace(numberString))
{
// 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
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz
- if (!string.IsNullOrWhiteSpace(nameInExtInf))
+ if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
{
var numberIndex = nameInExtInf.IndexOf(' ');
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
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
if (!IsValidChannelNumber(numberString))
{
@@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return false;
}
- if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
+ if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
return false;
}
@@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
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();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index a6e9779c9..eec880208 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -101,8 +101,8 @@
"TaskCleanTranscode": "Lösche Transkodier Pfad",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins",
- "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Erneuere Schausteller",
+ "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+ "TaskRefreshPeople": "Erneuere Schauspieler",
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index a22f66df9..a21cdad95 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -92,7 +92,7 @@
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
- "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+ "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新插件",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 62a23118f..90e2766b8 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
}
// Try splitting by : to handle "Germany: FSK 18"
- var index = rating.IndexOf(':');
+ var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
rating = rating.Substring(index).TrimStart(':').Trim();
@@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(culture));
}
- const string prefix = "Core";
- var key = prefix + culture;
+ const string Prefix = "Core";
+ var key = Prefix + culture;
return _dictionaries.GetOrAdd(
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)
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index b51c03446..4e25768cf 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
private Socket _socket;
- private int _localPort;
+ private readonly int _localPort;
private bool _disposed = false;
public Socket Socket => _socket;
- public IPAddress LocalIPAddress { get; }
-
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
SocketFlags = SocketFlags.None
@@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
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()
{
var receiveBuffer = new byte[8192];
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+ _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
var sendBuffer = new byte[8192];
_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;
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;
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)
{
ThrowIfDisposed();
@@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
}
}
+ /// <inheritdoc />
public void Dispose()
{
if (_disposed)
@@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
}
_socket?.Dispose();
+ _receiveSocketAsyncEventArgs.Dispose();
+ _sendSocketAsyncEventArgs.Dispose();
_currentReceiveTaskCompletionSource?.TrySetCanceled();
_currentSendTaskCompletionSource?.TrySetCanceled();
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
index 50cb44b28..089ec30e6 100644
--- a/Emby.Server.Implementations/Networking/NetworkManager.cs
+++ b/Emby.Server.Implementations/Networking/NetworkManager.cs
@@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
(octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927
{
- return false;
+ return true;
}
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
@@ -390,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host);
- address = GetIpAddresses(host).Result.FirstOrDefault();
+ address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null)
{
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 5dd1af4b8..38ceadedb 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{
- var playlist = new M3uPlaylist();
- playlist.IsExtended = true;
+ var playlist = new M3uPlaylist
+ {
+ IsExtended = true
+ };
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
{
- folderPath = folderPath + Path.DirectorySeparatorChar;
+ folderPath += Path.DirectorySeparatorChar;
}
var folderUri = new Uri(folderPath);
@@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
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)
{
- var typeName = "PlaylistsFolder";
+ const string TypeName = "PlaylistsFolder";
- 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));
+ 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));
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 3fe15ec68..81096026b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger<TaskManager> _logger;
- private readonly IFileSystem _fileSystem;
/// <summary>
/// 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="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The filesystem manager.</param>
public TaskManager(
IApplicationPaths applicationPaths,
IJsonSerializer jsonSerializer,
- ILogger<TaskManager> logger,
- IFileSystem fileSystem)
+ ILogger<TaskManager> logger)
{
_applicationPaths = applicationPaths;
_jsonSerializer = jsonSerializer;
_logger = logger;
- _fileSystem = fileSystem;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 3854be703..8439f8a99 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
@@ -25,11 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class ChapterImagesTask : IScheduledTask
{
/// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger<ChapterImagesTask> _logger;
-
- /// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
@@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
public ChapterImagesTask(
- ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IItemRepository itemRepo,
IApplicationPaths appPaths,
@@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
- _logger = loggerFactory.CreateLogger<ChapterImagesTask>();
_libraryManager = libraryManager;
_itemRepo = itemRepo;
_appPaths = appPaths;
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
index 857df591a..47e7261e8 100644
--- a/Emby.Server.Implementations/Services/ServiceController.cs
+++ b/Emby.Server.Implementations/Services/ServiceController.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
@@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
{
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)
{
- 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))
@@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
var service = httpHost.CreateInstance(serviceType);
- var serviceRequiresContext = service as IRequiresRequest;
- if (serviceRequiresContext != null)
+ if (service is IRequiresRequest serviceRequiresContext)
{
serviceRequiresContext.Request = req;
}
@@ -189,5 +199,4 @@ namespace Emby.Server.Implementations.Services
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
}
}
-
}
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
index cbc4b754d..7b970627e 100644
--- a/Emby.Server.Implementations/Services/ServiceExec.cs
+++ b/Emby.Server.Implementations/Services/ServiceExec.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services
}
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)
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
index a42f88ea0..b4166f771 100644
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ b/Emby.Server.Implementations/Services/ServiceHandler.cs
@@ -2,10 +2,12 @@
using System;
using System.Collections.Generic;
+using System.Net.Mime;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.Services
var pos = pathInfo.LastIndexOf('.');
if (pos != -1)
{
- var format = pathInfo.Substring(pos + 1);
+ var format = pathInfo.AsSpan().Slice(pos + 1);
contentType = GetFormatContentType(format);
if (contentType != null)
{
@@ -55,18 +57,21 @@ namespace Emby.Server.Implementations.Services
return pathInfo;
}
- private static string GetFormatContentType(string format)
+ private static string GetFormatContentType(ReadOnlySpan<char> format)
{
- // built-in formats
- switch (format)
+ if (format.Equals("json", StringComparison.Ordinal))
{
- case "json": return "application/json";
- case "xml": return "application/xml";
- default: return null;
+ return MediaTypeNames.Application.Json;
}
+ 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;
@@ -75,10 +80,11 @@ namespace Emby.Server.Implementations.Services
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);
+ httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
@@ -90,7 +96,7 @@ namespace Emby.Server.Implementations.Services
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;
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
index 89538ae72..442b2ab1c 100644
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ b/Emby.Server.Implementations/Services/ServicePath.cs
@@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services
{
var component = components[i];
- if (component.StartsWith(VariablePrefix))
+ if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
{
var variableName = component.Substring(1, component.Length - 2);
if (variableName[variableName.Length - 1] == WildCardChar)
@@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++)
{
- sb.Append(PathSeperatorChar + requestComponents[j]);
+ sb.Append(PathSeperatorChar)
+ .Append(requestComponents[j]);
}
value = sb.ToString();
@@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
- sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
+ sb.Append(PathSeperatorChar)
+ .Append(requestComponents[pathIx++]);
}
value = sb.ToString();
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index ca9f95c70..862a7296c 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">info</exception>
- /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
+ /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
public async Task OnPlaybackStopped(PlaybackStopInfo info)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index b9db6ecd0..8bebd37dc 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
if (session != null)
{
EnsureController(session, e.Argument);
- await KeepAliveWebSocket(e.Argument);
+ await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
}
else
{
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
// Notify WebSocket about timeout
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
if (_keepAliveCancellationToken != null)
{
_keepAliveCancellationToken.Cancel();
+ _keepAliveCancellationToken.Dispose();
_keepAliveCancellationToken = null;
}
}
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
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);
}
@@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
{
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
lock (_webSocketsLock)
{
- if (lost.Any())
+ if (lost.Count > 0)
{
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
foreach (var webSocket in lost)
@@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (!_webSockets.Any())
+ if (_webSockets.Count == 0)
{
StopKeepAlive();
}
@@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return webSocket.SendAsync(new WebSocketMessage<int>
- {
- MessageType = "ForceKeepAlive",
- Data = WebSocketLostTimeout
- }, CancellationToken.None);
+ return webSocket.SendAsync(
+ new WebSocketMessage<int>
+ {
+ MessageType = "ForceKeepAlive",
+ Data = WebSocketLostTimeout
+ },
+ CancellationToken.None);
}
/// <summary>
@@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
{
while (!cancellationToken.IsCancellationRequested)
{
- await callback();
- Task task = Task.Delay(interval, cancellationToken);
+ await callback().ConfigureAwait(false);
try
{
- await task;
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 2b7d818be..1f68a9c81 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
private static int CompareEpisodes(Episode x, Episode y)
{
- var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
- var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
+ var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
+ var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
return xValue.CompareTo(yValue);
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index 39d17833f..80b977731 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
/// All sessions will receive the message.
/// </summary>
AllGroup = 0,
+
/// <summary>
/// Only the specified session will receive the message.
/// </summary>
CurrentSession = 1,
+
/// <summary>
/// All sessions, except the current one, will receive the message.
/// </summary>
AllExceptCurrentSession = 2,
+
/// <summary>
/// Only sessions that are not buffering will receive the message.
/// </summary>
@@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
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>
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
/// </summary>
@@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
_syncPlayManager = syncPlayManager;
}
+ /// <inheritdoc />
+ public Guid GetGroupId() => _group.GroupId;
+
+ /// <inheritdoc />
+ public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+ /// <inheritdoc />
+ public bool IsGroupEmpty() => _group.IsEmpty();
+
/// <summary>
/// Converts DateTime to UTC string.
/// </summary>
@@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <value>The UTC string.</value>
private string DateToUTCString(DateTime date)
{
- return date.ToUniversalTime().ToString("o");
+ return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
}
/// <summary>
@@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
/// <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)
{
case BroadcastType.CurrentSession:
return new SessionInfo[] { from };
case BroadcastType.AllGroup:
- return _group.Participants.Values.Select(
- session => session.Session).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session);
case BroadcastType.AllExceptCurrentSession:
- return _group.Participants.Values.Select(
- session => session.Session).Where(
- session => !session.Id.Equals(from.Id)).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session)
+ .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
case BroadcastType.AllReady:
- return _group.Participants.Values.Where(
- session => !session.IsBuffering).Select(
- session => session.Session).ToArray();
+ return _group.Participants.Values
+ .Where(session => !session.IsBuffering)
+ .Select(session => session.Session);
default:
return Array.Empty<SessionInfo>();
}
@@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- 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()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- 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
{
- var playRequest = new PlayRequest();
- playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
- playRequest.StartPositionTicks = _group.PositionTicks;
+ var playRequest = new PlayRequest
+ {
+ ItemIds = new Guid[] { _group.PlayingItem.Id },
+ StartPositionTicks = _group.PositionTicks
+ };
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 1255e6dab..62f6097f3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -1,8 +1,12 @@
+using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
-using System.Threading;
+using System.Globalization;
+using System.Linq;
using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Persistence;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -17,15 +21,15 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DisplayPreferencesController : BaseJellyfinApiController
{
- private readonly IDisplayPreferencesRepository _displayPreferencesRepository;
+ private readonly IDisplayPreferencesManager _displayPreferencesManager;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary>
- /// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param>
- public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository)
+ /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
{
- _displayPreferencesRepository = displayPreferencesRepository;
+ _displayPreferencesManager = displayPreferencesManager;
}
/// <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>
[HttpGet("{displayPreferencesId}")]
[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,
- [FromQuery] [Required] string? userId,
+ [FromQuery] [Required] Guid userId,
[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>
@@ -60,15 +99,77 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult UpdateDisplayPreferences(
[FromRoute] string? displayPreferencesId,
- [FromQuery, BindRequired] string? userId,
+ [FromQuery, BindRequired] Guid userId,
[FromQuery, BindRequired] string? client,
- [FromBody, BindRequired] DisplayPreferences displayPreferences)
+ [FromBody, BindRequired] DisplayPreferencesDto displayPreferences)
{
- _displayPreferencesRepository.SaveDisplayPreferences(
- displayPreferences,
- userId,
- client,
- CancellationToken.None);
+ HomeSectionType[] defaults =
+ {
+ HomeSectionType.SmallLibraryTiles,
+ HomeSectionType.Resume,
+ 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();
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index a9af1a2c3..148d8a18e 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 252b5cdd1..f9e4e61b5 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -53,14 +53,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
{
- var result = new StartupConfigurationDto
+ return new StartupConfigurationDto
{
UICulture = _config.Configuration.UICulture,
MetadataCountryCode = _config.Configuration.MetadataCountryCode,
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
};
-
- return result;
}
/// <summary>
@@ -110,10 +108,10 @@ namespace Jellyfin.Api.Controllers
[HttpGet("User")]
[HttpGet("FirstUser")]
[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.
- _userManager.Initialize();
+ await _userManager.InitializeAsync().ConfigureAwait(false);
var user = _userManager.Users.First();
return new StartupUserDto
{
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 1c38b8de5..b62ff80fc 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -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)");
}
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+ builder.AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:3")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
long positionTicks = 0;
@@ -291,7 +292,9 @@ namespace Jellyfin.Api.Controllers
var remaining = runtime - positionTicks;
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);
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index bf3c1e2b1..55759f316 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -2,11 +2,11 @@
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index d54bc10c0..508b5e24e 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 8038ca044..ce0c9281b 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -455,7 +455,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
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
if (request.Password != null)
diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
new file mode 100644
index 000000000..249d828d3
--- /dev/null
+++ b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
@@ -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; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
new file mode 100644
index 000000000..cda83f6bb
--- /dev/null
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -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; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
new file mode 100644
index 000000000..062046260
--- /dev/null
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -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; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
new file mode 100644
index 000000000..95c08e6c6
--- /dev/null
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -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; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index b89b0a8f4..50810561f 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
PasswordResetProviderId = passwordResetProviderId;
AccessSchedules = new HashSet<AccessSchedule>();
+ ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
// Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
Preferences = new HashSet<Preference>();
@@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
// [ForeignKey("UserId")]
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]
public SyncPlayAccess SyncPlayAccess { get; set; }
@@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
/// </summary>
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>
/// Gets or sets the list of groups this user is a member of.
diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Data/Enums/ChromecastVersion.cs
new file mode 100644
index 000000000..2622e08ef
--- /dev/null
+++ b/Jellyfin.Data/Enums/ChromecastVersion.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs
new file mode 100644
index 000000000..e597c9431
--- /dev/null
+++ b/Jellyfin.Data/Enums/HomeSectionType.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs
new file mode 100644
index 000000000..9badc6573
--- /dev/null
+++ b/Jellyfin.Data/Enums/IndexingKind.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Data/Enums/ScrollDirection.cs b/Jellyfin.Data/Enums/ScrollDirection.cs
new file mode 100644
index 000000000..9595eb490
--- /dev/null
+++ b/Jellyfin.Data/Enums/ScrollDirection.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Data/Enums/SortOrder.cs b/Jellyfin.Data/Enums/SortOrder.cs
new file mode 100644
index 000000000..760a857f5
--- /dev/null
+++ b/Jellyfin.Data/Enums/SortOrder.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs
new file mode 100644
index 000000000..595429ab1
--- /dev/null
+++ b/Jellyfin.Data/Enums/ViewType.cs
@@ -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
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 6db514b2e..c71c76f08 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,11 +18,10 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BlurHashSharp" Version="1.0.1" />
- <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
- <PackageReference Include="SkiaSharp" Version="1.68.3" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
- <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
+ <PackageReference Include="BlurHashSharp" Version="1.1.0" />
+ <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
+ <PackageReference Include="SkiaSharp" Version="2.80.1" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
index f2df066ec..6136a2ff9 100644
--- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
@@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia
/// <param name="percent">The percentage played to display with the indicator.</param>
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
{
- using (var paint = new SKPaint())
- {
- var endX = imageSize.Width - 1;
- var endY = imageSize.Height - 1;
+ using var paint = new SKPaint();
+ var endX = imageSize.Width - 1;
+ var endY = imageSize.Height - 1;
- paint.Color = SKColor.Parse("#99000000");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
+ paint.Color = SKColor.Parse("#99000000");
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
- double foregroundWidth = endX;
- foregroundWidth *= percent;
- foregroundWidth /= 100;
+ double foregroundWidth = (endX * percent) / 100;
- paint.Color = SKColor.Parse("#FF00A4DC");
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
- }
+ paint.Color = SKColor.Parse("#FF00A4DC");
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
}
}
}
diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
index 7eed5f4f7..db4f78398 100644
--- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
+++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
@@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia
{
var x = imageSize.Width - OffsetFromTopRightCorner;
- using (var paint = new SKPaint())
+ using var paint = new SKPaint
{
- paint.Color = SKColor.Parse("#CC00A4DC");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- }
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
- using (var paint = new SKPaint())
- {
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.Style = SKPaintStyle.Fill;
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- paint.TextSize = 30;
- paint.IsAntialias = true;
+ paint.Color = new SKColor(255, 255, 255, 255);
+ paint.TextSize = 30;
+ paint.IsAntialias = true;
- // or:
- // var emojiChar = 0x1F680;
- const string Text = "✔️";
- var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
+ // or:
+ // var emojiChar = 0x1F680;
+ const string Text = "✔️";
+ var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
- // ask the font manager for a font with that character
- paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
+ // ask the font manager for a font with that character
+ paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
- canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
- }
+ canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
}
}
}
diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
index 1d2db5515..9a50a4d62 100644
--- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs
+++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
/// </summary>
/// <param name="result">The non-successful codec result returned by Skia.</param>
- public SkiaCodecException(SKCodecResult result) : base()
+ public SkiaCodecException(SKCodecResult result)
{
CodecResult = result;
}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index ba9a5809f..a1caa751b 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary>
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
- public SkiaEncoder(
- ILogger<SkiaEncoder> logger,
- IApplicationPaths appPaths)
+ public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
{
_logger = logger;
_appPaths = appPaths;
@@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
/// <returns>The converted format.</returns>
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
{
- switch (selectedFormat)
- {
- case ImageFormat.Bmp:
- return SKEncodedImageFormat.Bmp;
- case ImageFormat.Jpg:
- return SKEncodedImageFormat.Jpeg;
- case ImageFormat.Gif:
- return SKEncodedImageFormat.Gif;
- case ImageFormat.Webp:
- return SKEncodedImageFormat.Webp;
- default:
- return SKEncodedImageFormat.Png;
- }
+ return selectedFormat switch
+ {
+ ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
+ ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
+ ImageFormat.Gif => SKEncodedImageFormat.Gif,
+ ImageFormat.Webp => SKEncodedImageFormat.Webp,
+ _ => SKEncodedImageFormat.Png
+ };
}
private static bool IsTransparentRow(SKBitmap bmp, int row)
@@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
{
var topmost = 0;
- for (int row = 0; row < bitmap.Height; ++row)
+ while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
{
- if (IsTransparentRow(bitmap, row))
- {
- topmost = row + 1;
- }
- else
- {
- break;
- }
+ topmost++;
}
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 = row;
- }
- else
- {
- break;
- }
+ bottommost--;
}
- int leftmost = 0, rightmost = bitmap.Width;
- for (int col = 0; col < bitmap.Width; ++col)
+ var leftmost = 0;
+ while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
{
- if (IsTransparentColumn(bitmap, col))
- {
- leftmost = col + 1;
- }
- else
- {
- break;
- }
+ leftmost++;
}
- 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 = col;
- }
- else
- {
- break;
- }
+ rightmost--;
}
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
- using (var image = SKImage.FromBitmap(bitmap))
- using (var subset = image.Subset(newRect))
- {
- return SKBitmap.FromImage(subset);
- }
+ using var image = SKImage.FromBitmap(bitmap);
+ using var subset = image.Subset(newRect);
+ return SKBitmap.FromImage(subset);
}
/// <inheritdoc />
@@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
throw new FileNotFoundException("File not found", path);
}
- using (var codec = SKCodec.Create(path, out SKCodecResult result))
- {
- EnsureSuccess(result);
+ using var codec = SKCodec.Create(path, out SKCodecResult 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 />
@@ -237,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
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)
@@ -253,12 +216,7 @@ namespace Jellyfin.Drawing.Skia
}
}
- if (HasDiacritics(path))
- {
- return true;
- }
-
- return false;
+ return HasDiacritics(path);
}
private string NormalizePath(string path)
@@ -283,25 +241,17 @@ namespace Jellyfin.Drawing.Skia
return SKEncodedOrigin.TopLeft;
}
- switch (orientation.Value)
- {
- case ImageOrientation.TopRight:
- return SKEncodedOrigin.TopRight;
- case ImageOrientation.RightTop:
- return SKEncodedOrigin.RightTop;
- case ImageOrientation.RightBottom:
- return SKEncodedOrigin.RightBottom;
- case ImageOrientation.LeftTop:
- return SKEncodedOrigin.LeftTop;
- case ImageOrientation.LeftBottom:
- return SKEncodedOrigin.LeftBottom;
- case ImageOrientation.BottomRight:
- return SKEncodedOrigin.BottomRight;
- case ImageOrientation.BottomLeft:
- return SKEncodedOrigin.BottomLeft;
- default:
- return SKEncodedOrigin.TopLeft;
- }
+ return orientation.Value switch
+ {
+ ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
+ ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
+ ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
+ ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
+ ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
+ ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
+ ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
+ _ => SKEncodedOrigin.TopLeft
+ };
}
/// <summary>
@@ -323,24 +273,22 @@ namespace Jellyfin.Drawing.Skia
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);
+ // create the bitmap
+ var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
- // decode
- _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+ // decode
+ _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
- origin = codec.EncodedOrigin;
+ origin = codec.EncodedOrigin;
- return bitmap;
- }
+ return bitmap;
}
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
@@ -367,15 +315,8 @@ namespace Jellyfin.Drawing.Skia
{
if (cropWhitespace)
{
- using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
- {
- if (bitmap == null)
- {
- return null;
- }
-
- return CropWhiteSpace(bitmap);
- }
+ using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
+ return bitmap == null ? null : CropWhiteSpace(bitmap);
}
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
@@ -403,133 +344,105 @@ namespace Jellyfin.Drawing.Skia
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)
{
case SKEncodedOrigin.TopRight:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(rotated.Width, 0);
- surface.Scale(-1, 1);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.Scale(-1, 1, midX, midY);
+ break;
case SKEncodedOrigin.BottomRight:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- 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;
- }
-
+ surface.RotateDegrees(180, midX, midY);
+ break;
case SKEncodedOrigin.BottomLeft:
- {
- var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
- 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;
- }
-
+ surface.Scale(1, -1, midX, midY);
+ break;
case SKEncodedOrigin.LeftTop:
- {
- // TODO: Remove dual canvases, had trouble with flipping
- using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
- {
- 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;
- }
- }
-
+ surface.Translate(0, -rotated.Height);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(-90);
+ break;
case SKEncodedOrigin.RightTop:
- {
- var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(rotated.Width, 0);
- surface.RotateDegrees(90);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
+ surface.Translate(rotated.Width, 0);
+ surface.RotateDegrees(90);
+ break;
case SKEncodedOrigin.RightBottom:
- {
- // TODO: Remove dual canvases, had trouble with flipping
- using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
- {
- 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;
- }
- }
-
+ surface.Translate(rotated.Width, 0);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(90);
+ break;
case SKEncodedOrigin.LeftBottom:
- {
- var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
- using (var surface = new SKCanvas(rotated))
- {
- surface.Translate(0, rotated.Height);
- surface.RotateDegrees(270);
- surface.DrawBitmap(bitmap, 0, 0);
- }
-
- return rotated;
- }
-
- default: return bitmap;
+ surface.Translate(0, rotated.Height);
+ surface.RotateDegrees(-90);
+ break;
}
+
+ 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/>
@@ -552,97 +465,87 @@ namespace Jellyfin.Drawing.Skia
var blur = options.Blur ?? 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}");
- }
+ throw new InvalidDataException($"Skia unable to read image {inputPath}");
+ }
- var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
+ 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;
- }
+ 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;
- var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+ // 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));
- var width = newImageSize.Width;
- var height = newImageSize.Height;
+ // 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));
+ }
- using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+ // 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))
{
- // scale image
- bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
+ opacity = .4;
+ }
- // 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;
- }
- }
+ canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+ }
- // 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);
- }
- }
- }
+ 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);
}
}
diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs
index 968d3a244..5b272eac5 100644
--- a/Jellyfin.Drawing.Skia/SkiaException.cs
+++ b/Jellyfin.Drawing.Skia/SkiaException.cs
@@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia
/// <summary>
/// Initializes a new instance of the <see cref="SkiaException"/> class.
/// </summary>
- public SkiaException() : base()
+ public SkiaException()
{
}
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 61bef90ec..b08c3750d 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia
/// <param name="height">The desired height of the collage.</param>
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
{
- using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
- using (var outputStream = new SKFileWStream(outputPath))
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
+ using var bitmap = BuildSquareCollageBitmap(paths, width, height);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
/// <summary>
@@ -86,56 +84,44 @@ namespace Jellyfin.Drawing.Skia
/// <param name="height">The desired height of the collage.</param>
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
{
- using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
- using (var outputStream = new SKFileWStream(outputPath))
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
+ using var bitmap = BuildThumbCollageBitmap(paths, width, height);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
{
var bitmap = new SKBitmap(width, height);
- using (var canvas = new SKCanvas(bitmap))
- {
- canvas.Clear(SKColors.Black);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
- // number of images used in the thumbnail
- var iCount = 3;
+ // 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++)
+ // 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);
+ imageIndex = newIndex;
+ if (currentBitmap == null)
{
- using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
- {
- 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);
- }
- }
- }
+ continue;
}
+
+ // 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;
@@ -176,33 +162,27 @@ namespace Jellyfin.Drawing.Skia
var cellWidth = width / 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))
- {
- 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);
- }
- }
+ continue;
}
+
+ // 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);
}
}
diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
index cf3dbde2c..58f887c96 100644
--- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia
var x = imageSize.Width - OffsetFromTopRightCorner;
var text = count.ToString(CultureInfo.InvariantCulture);
- using (var paint = new SKPaint())
+ using var paint = new SKPaint
{
- paint.Color = SKColor.Parse("#CC00A4DC");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- }
-
- using (var paint = new SKPaint())
- {
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.Style = SKPaintStyle.Fill;
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
- paint.TextSize = 24;
- paint.IsAntialias = true;
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
- var y = OffsetFromTopRightCorner + 9;
+ paint.Color = new SKColor(255, 255, 255, 255);
+ paint.TextSize = 24;
+ paint.IsAntialias = true;
- if (text.Length == 1)
- {
- x -= 7;
- }
+ var y = OffsetFromTopRightCorner + 9;
- if (text.Length == 2)
- {
- x -= 13;
- }
- else if (text.Length >= 3)
- {
- x -= 15;
- y -= 2;
- paint.TextSize = 18;
- }
+ if (text.Length == 1)
+ {
+ x -= 7;
+ }
- canvas.DrawText(text, x, y, paint);
+ if (text.Length == 2)
+ {
+ x -= 13;
+ }
+ else if (text.Length >= 3)
+ {
+ x -= 15;
+ y -= 2;
+ paint.TextSize = 18;
}
+
+ canvas.DrawText(text, x, y, paint);
}
}
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 53120a763..7d864ebc6 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.Linq;
using Jellyfin.Data;
using Jellyfin.Data.Entities;
@@ -27,8 +28,12 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+ public virtual DbSet<DisplayPreferences> DisplayPreferences { 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<Preference> Preferences { get; set; }
@@ -133,6 +138,18 @@ namespace Jellyfin.Server.Implementations
return base.SaveChanges();
}
+ /// <inheritdoc/>
+ public override void Dispose()
+ {
+ foreach (var entry in ChangeTracker.Entries())
+ {
+ entry.State = EntityState.Detached;
+ }
+
+ GC.SuppressFinalize(this);
+ base.Dispose();
+ }
+
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index 8f5c19900..486be6053 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations
public class JellyfinDbProvider
{
private readonly IServiceProvider _serviceProvider;
+ private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <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.GetRequiredService<JellyfinDb>().Database.Migrate();
+ _appPaths = appPaths;
+
+ using var jellyfinDb = CreateContext();
+ jellyfinDb.Database.Migrate();
}
/// <summary>
@@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations
/// <returns>The newly created context.</returns>
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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
new file mode 100644
index 000000000..d44707d06
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
@@ -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
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
new file mode 100644
index 000000000..3009f0b61
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs
@@ -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");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 51fad8224..a6e6a2324 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "3.1.4");
+ .HasAnnotation("ProductVersion", "3.1.6");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
@@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
@@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
.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)
@@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
.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)
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index cf5a01f08..6cb13cd23 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -4,13 +4,14 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
+using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -22,8 +23,7 @@ namespace Jellyfin.Server.Implementations.Users
{
private const string BaseResetFileName = "passwordreset";
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IUserManager _userManager;
+ private readonly IApplicationHost _appHost;
private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir;
@@ -32,17 +32,13 @@ namespace Jellyfin.Server.Implementations.Users
/// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
- /// <param name="jsonSerializer">The JSON serializer.</param>
- /// <param name="userManager">The user manager.</param>
- public DefaultPasswordResetProvider(
- IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
- IUserManager userManager)
+ /// <param name="appHost">The application host.</param>
+ public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost)
{
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
- _jsonSerializer = jsonSerializer;
- _userManager = userManager;
+ _appHost = appHost;
+ // TODO: Remove the circular dependency on UserManager
}
/// <inheritdoc />
@@ -54,13 +50,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
+ var userManager = _appHost.Resolve<IUserManager>();
var usersReset = new List<string>();
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
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)
@@ -72,10 +69,10 @@ namespace Jellyfin.Server.Implementations.Users
pin.Replace("-", string.Empty, StringComparison.Ordinal),
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");
- await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
+ await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersReset.Add(resetUser.Username);
File.Delete(resetFile);
}
@@ -116,12 +113,11 @@ namespace Jellyfin.Server.Implementations.Users
await using (FileStream fileStream = File.OpenWrite(filePath))
{
- _jsonSerializer.SerializeToStream(spr, fileStream);
+ await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return new ForgotPasswordResult
{
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
new file mode 100644
index 000000000..7c5c5a3ec
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -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();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 343f452c7..11402ee05 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly ILogger<UserManager> _logger;
-
- private IAuthenticationProvider[] _authenticationProviders = null!;
- private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
- private InvalidAuthProvider _invalidAuthProvider = null!;
- private IPasswordResetProvider[] _passwordResetProviders = null!;
- private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
+ private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
+ private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
+ private readonly InvalidAuthProvider _invalidAuthProvider;
+ private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
+ private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
@@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost;
_imageProcessor = imageProcessor;
_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/>
@@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <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/>
public User? GetUserById(Guid id)
@@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
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/>
- public User CreateUser(string name)
+ public async Task<User> CreateUserAsync(string name)
{
if (!IsValidUsername(name))
{
@@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
using var dbContext = _dbProvider.CreateContext();
- // TODO: Remove after user item data is migrated.
- var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
+ var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
- var newUser = new User(
- name,
- _defaultAuthenticationProvider.GetType().FullName,
- _defaultPasswordResetProvider.GetType().FullName)
- {
- InternalId = max + 1
- };
dbContext.Users.Add(newUser);
- dbContext.SaveChanges();
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
@@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
}
else
{
- IncrementInvalidLoginAttemptCount(user);
+ await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username,
@@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
if (user != null && isInNetwork)
{
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
@@ -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 />
- public void Initialize()
+ public async Task InitializeAsync()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
using var dbContext = _dbProvider.CreateContext();
- if (dbContext.Users.Any())
+ if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
var defaultName = Environment.UserName;
- if (string.IsNullOrWhiteSpace(defaultName))
+ if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
- if (!IsValidUsername(defaultName))
- {
- throw new ArgumentException("Provided username is not valid!", defaultName);
- }
-
- var newUser = CreateUser(defaultName);
+ var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
- dbContext.Users.Update(newUser);
- dbContext.SaveChanges();
+ dbContext.Users.Add(newUser);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -637,7 +649,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdateConfiguration(Guid userId, UserConfiguration config)
{
- var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
@@ -670,7 +682,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy)
{
- var dbContext = _dbProvider.CreateContext();
+ using var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users
.Include(u => u.Permissions)
.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 @
// 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 (.)
- return Regex.IsMatch(name, @"^[\w\-'._@]*$");
+ // 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\ \-'._@]*$");
}
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++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@@ -896,7 +908,7 @@ namespace Jellyfin.Server.Implementations.Users
user.InvalidLoginAttemptCount);
}
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 207eaa98d..29a59e1c8 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
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="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
- StartupOptions options,
+ IStartupOptions options,
IFileSystem fileSystem,
INetworkManager networkManager)
: base(
@@ -63,16 +64,18 @@ namespace Jellyfin.Server
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
}
- // TODO: Set up scoping and use AddDbContextPool
- serviceCollection.AddDbContext<JellyfinDb>(
- options => options
- .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
- ServiceLifetime.Transient);
+ // TODO: Set up scoping and use AddDbContextPool,
+ // can't register as Transient since tracking transient in GC is funky
+ // serviceCollection.AddDbContext<JellyfinDb>(
+ // options => options
+ // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
+ // ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
+ serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
base.RegisterServices(serviceCollection);
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index b1bd38cff..7541707d9 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -45,7 +45,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="prometheus-net" 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.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index fe8910c3c..844dae61f 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -21,7 +21,9 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.MigrateActivityLogDb),
typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.AddDefaultPluginRepository),
- typeof(Routines.MigrateUserDb)
+ typeof(Routines.MigrateUserDb),
+ typeof(Routines.ReaddDefaultPluginRepository),
+ typeof(Routines.MigrateDisplayPreferencesDb)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
new file mode 100644
index 000000000..b15ccf01e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -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'");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
new file mode 100644
index 000000000..b281b5cc0
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -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();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 288a5c259..9fd706d36 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -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))
.UseSerilog()
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 418cd92b3..c80e8e64f 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -194,7 +194,8 @@ namespace MediaBrowser.Api.Playback.Hls
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
// 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");
builder.AppendLine(playlistUrl);
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index a347b365d..97ae0f0fd 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -361,7 +361,7 @@ namespace MediaBrowser.Api.Playback.Hls
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);
}
@@ -960,7 +960,8 @@ namespace MediaBrowser.Api.Playback.Hls
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
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");
var queryStringIndex = Request.RawUrl.IndexOf('?');
@@ -975,14 +976,17 @@ namespace MediaBrowser.Api.Playback.Hls
foreach (var length in segmentLengths)
{
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
-
- builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
+ builder.Append("#EXTINF:")
+ .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+ .AppendLine(", nodesc");
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "hls1/{0}/{1}{2}{3}",
name,
index.ToString(CultureInfo.InvariantCulture),
GetSegmentFileExtension(request),
- queryString));
+ queryString).AppendLine();
index++;
}
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
new file mode 100644
index 000000000..d746207c7
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Static class containing extension methods for <see cref="HttpContext"/>.
+ /// </summary>
+ public static class HttpContextExtensions
+ {
+ private const string ServiceStackRequest = "ServiceStackRequest";
+
+ /// <summary>
+ /// Set the ServiceStack request.
+ /// </summary>
+ /// <param name="httpContext">The HttpContext instance.</param>
+ /// <param name="request">The service stack request instance.</param>
+ public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
+ {
+ httpContext.Items[ServiceStackRequest] = request;
+ }
+
+ /// <summary>
+ /// Get the ServiceStack request.
+ /// </summary>
+ /// <param name="httpContext">The HttpContext instance.</param>
+ /// <returns>The service stack request instance.</returns>
+ public static IRequest GetServiceStackRequest(this HttpContext httpContext)
+ {
+ return (IRequest)httpContext.Items[ServiceStackRequest];
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 90f62e153..f34309c40 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -675,11 +675,11 @@ namespace MediaBrowser.Controller.Entities
return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
}
- var idString = Id.ToString("N", CultureInfo.InvariantCulture);
+ ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
basePath = System.IO.Path.Combine(basePath, "library");
- return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
+ return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString);
}
/// <summary>
@@ -702,26 +702,27 @@ namespace MediaBrowser.Controller.Entities
foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
{
- sortable = sortable.Replace(removeChar, string.Empty);
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
}
foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
{
- sortable = sortable.Replace(replaceChar, " ");
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
}
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
- if (sortable.StartsWith(search + " "))
+ if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
{
sortable = sortable.Remove(0, search.Length + 1);
}
+
// Remove from middle if surrounded by spaces
- sortable = sortable.Replace(" " + search + " ", " ");
+ sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
// Remove from end if followed by a space
- if (sortable.EndsWith(" " + search))
+ if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
{
sortable = sortable.Remove(sortable.Length - (search.Length + 1));
}
@@ -751,6 +752,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunkBuilder);
}
+
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index cb35d6e32..22bb7fd55 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
new file mode 100644
index 000000000..b6bfed3e5
--- /dev/null
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Manages the storage and retrieval of display preferences.
+ /// </summary>
+ public interface IDisplayPreferencesManager
+ {
+ /// <summary>
+ /// Gets the display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user's id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>The associated display preferences.</returns>
+ DisplayPreferences GetDisplayPreferences(Guid userId, string client);
+
+ /// <summary>
+ /// Gets the default item display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>The item display preferences.</returns>
+ ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client);
+
+ /// <summary>
+ /// Gets all of the item display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>A list of item display preferences.</returns>
+ IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
+
+ /// <summary>
+ /// Saves changes to the provided display preferences.
+ /// </summary>
+ /// <param name="preferences">The display preferences to save.</param>
+ void SaveChanges(DisplayPreferences preferences);
+
+ /// <summary>
+ /// Saves changes to the provided item display preferences.
+ /// </summary>
+ /// <param name="preferences">The item display preferences to save.</param>
+ void SaveChanges(ItemDisplayPreferences preferences);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 9d6646857..9abcf2b62 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -199,7 +200,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Updates the item.
/// </summary>
- void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+ void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index e73fe7120..6685861a9 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
@@ -55,7 +54,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Initializes the user manager and ensures that a user exists.
/// </summary>
- void Initialize();
+ Task InitializeAsync();
/// <summary>
/// Gets a user by Id.
@@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>The created user.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
- User CreateUser(string name);
+ Task<User> CreateUserAsync(string name);
/// <summary>
/// Deletes the specified user.
@@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
- void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
-
NameIdPair[] GetAuthenticationProviders();
NameIdPair[] GetPasswordResetProviders();
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d534e5a59..2dd21be3c 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -372,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public int GetVideoProfileScore(string profile)
{
// strip spaces because they may be stripped out on the query string
- profile = profile.Replace(" ", "");
+ profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal);
return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
@@ -456,11 +456,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
if (!IsCopyCodec(outputVideoCodec))
{
if (state.IsVideoRequest
- && IsVaapiSupported(state)
+ && _mediaEncoder.SupportsHwaccel("vaapi")
&& string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{
if (isVaapiDecoder)
@@ -468,13 +469,13 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append("-hwaccel_output_format vaapi ")
.Append("-vaapi_device ")
.Append(encodingOptions.VaapiDevice)
- .Append(" ");
+ .Append(' ');
}
else if (!isVaapiDecoder && isVaapiEncoder)
{
arg.Append("-vaapi_device ")
.Append(encodingOptions.VaapiDevice)
- .Append(" ");
+ .Append(' ');
}
}
@@ -495,13 +496,13 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else
{
- arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
+ arg.Append("-hwaccel qsv ");
}
}
if (isWindows)
{
- arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
+ arg.Append("-hwaccel qsv ");
}
}
// While using SW decoder
@@ -511,6 +512,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
}
+
+ if (state.IsVideoRequest
+ && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ arg.Append("-hwaccel videotoolbox ");
+ }
}
arg.Append("-i ")
@@ -1599,7 +1606,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
outputVideoCodec ??= string.Empty;
- var outputSizeParam = string.Empty;
+ var outputSizeParam = ReadOnlySpan<char>.Empty;
var request = state.BaseRequest;
// Add resolution params, if specified
@@ -1610,31 +1617,53 @@ namespace MediaBrowser.Controller.MediaEncoding
{
outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
- var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
+ // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
+ var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+ // vpp_qsv
+ index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+ // hwdownload,format=p010le (hardware decode + software encode for vaapi)
+ index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
}
else
{
- index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ // format=nv12|vaapi,hwupload,scale_vaapi
+ index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- outputSizeParam = outputSizeParam.Substring(index);
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ else
+ {
+ // yadif,scale=expr
+ index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ else
+ {
+ // scale=expr
+ index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = outputSizeParam.Slice(index);
+ }
+ }
}
}
}
@@ -1680,9 +1709,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
// Always put the scaler before the overlay for better performance
- var retStr = !string.IsNullOrEmpty(outputSizeParam) ?
- " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" :
- " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
+ var retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@@ -1696,7 +1725,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
+ else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{
/*
@@ -1716,7 +1745,7 @@ namespace MediaBrowser.Controller.MediaEncoding
*/
if (isLinux)
{
- retStr = !string.IsNullOrEmpty(outputSizeParam) ?
+ retStr = !outputSizeParam.IsEmpty ?
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
}
@@ -1728,7 +1757,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mapPrefix,
subtitleStreamIndex,
state.VideoStream.Index,
- outputSizeParam,
+ outputSizeParam.ToString(),
videoSizeParam);
}
@@ -1801,6 +1830,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputWidth = width.Value;
var outputHeight = height.Value;
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var isDeintEnabled = state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true);
if (!videoWidth.HasValue
|| outputWidth != videoWidth.Value
@@ -1816,7 +1849,7 @@ namespace MediaBrowser.Controller.MediaEncoding
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
outputWidth,
outputHeight,
- (qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
else
{
@@ -1825,7 +1858,7 @@ namespace MediaBrowser.Controller.MediaEncoding
CultureInfo.InvariantCulture,
"{0}=format=nv12{1}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
- (qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
+ (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
}
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -2037,7 +2070,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
var request = state.BaseRequest;
-
var videoStream = state.VideoStream;
var filters = new List<string>();
@@ -2046,23 +2078,31 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+ var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
// When the input may or may not be hardware VAAPI decodable
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
- // When the input may or may not be hardware QSV decodable
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
+ else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
{
- filters.Add("format=nv12|qsv");
filters.Add("hwupload=extra_hw_frames=64");
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
{
var codec = videoStream.Codec.ToLowerInvariant();
var isColorDepth10 = IsColorDepth10(state);
@@ -2090,18 +2130,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true))
+ if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
{
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
}
}
// Add software deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true))
+ if (state.DeInterlace("h264", true)
+ || state.DeInterlace("avc", true)
+ || state.DeInterlace("h265", true)
+ || state.DeInterlace("hevc", true))
{
- var deintParam = string.Empty;
+ string deintParam;
var inputFramerate = videoStream?.RealFrameRate;
// If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
@@ -2116,9 +2159,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(deintParam))
{
- if (!string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- && videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) == -1)
+ if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
{
filters.Add(deintParam);
}
@@ -2128,12 +2169,10 @@ namespace MediaBrowser.Controller.MediaEncoding
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
- // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
+ if (isVaapiH264Encoder)
{
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
// Test passed on Intel and AMD gfx
filters.Add("hwmap=mode=read+write");
@@ -2143,9 +2182,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var output = string.Empty;
- if (state.SubtitleStream != null
- && state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ if (hasTextSubs)
{
var subParam = GetTextSubtitleParam(state);
@@ -2153,7 +2190,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (isVaapiH264Encoder)
{
filters.Add("hwmap");
}
@@ -2170,7 +2207,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return output;
}
-
/// <summary>
/// Gets the number of threads.
/// </summary>
diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
deleted file mode 100644
index c2dcb66d7..000000000
--- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
- /// <summary>
- /// Interface IDisplayPreferencesRepository.
- /// </summary>
- public interface IDisplayPreferencesRepository : IRepository
- {
- /// <summary>
- /// Saves display preferences for an item.
- /// </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>
- void SaveDisplayPreferences(
- DisplayPreferences displayPreferences,
- string userId,
- string client,
- CancellationToken cancellationToken);
-
- /// <summary>
- /// Saves all display preferences for a user.
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveAllDisplayPreferences(
- IEnumerable<DisplayPreferences> displayPreferences,
- Guid userId,
- CancellationToken cancellationToken);
-
- /// <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>
- DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
-
- /// <summary>
- /// Gets all display preferences for the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
- }
-}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index b1a638883..0fd63770f 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 0fd0239b4..5c43fdcfa 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
internal static Version GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
- var match = Regex.Match(output, @"^ffmpeg version n?((?:\d+\.?)+)");
+ var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
if (match.Success)
{
@@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var rc = new StringBuilder(144);
foreach (Match m in Regex.Matches(
output,
- @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
+ @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
RegexOptions.Multiline))
{
rc.Append(m.Groups["name"])
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index c85d963ed..0b447e3e6 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1367,7 +1367,9 @@ namespace MediaBrowser.MediaEncoding.Probing
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
- if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ if (string.IsNullOrWhiteSpace(subTitle)
+ && !string.IsNullOrWhiteSpace(description)
+ && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] parts = description.Split(':');
if (parts.Length > 0)
@@ -1375,7 +1377,7 @@ namespace MediaBrowser.MediaEncoding.Probing
string subtitle = parts[0];
try
{
- if (subtitle.Contains("/")) // It contains a episode number and season number
+ if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
{
string[] numbers = subtitle.Split(' ');
video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
@@ -1390,8 +1392,11 @@ namespace MediaBrowser.MediaEncoding.Probing
}
catch // Default parsing
{
- if (subtitle.Contains(".")) // skip the comment, keep the subtitle
+ if (subtitle.Contains('.', StringComparison.Ordinal))
+ {
+ // skip the comment, keep the subtitle
description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+ }
else
{
description = subtitle.Trim(); // Clean up whitespaces and save it
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index 0e2d70017..43a45291c 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -35,7 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
continue;
}
- if (line.StartsWith("["))
+ if (line[0] == '[')
{
break;
}
@@ -62,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return trackInfo;
}
- long GetTicks(string time)
+ private long GetTicks(ReadOnlySpan<char> time)
{
return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span)
? span.Ticks : 0;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
index bf8808eb8..dca5c1e8a 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
@@ -1,6 +1,9 @@
+#nullable enable
+#pragma warning disable CS1591
+
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class ParserValues
+ public static class ParserValues
{
public const string NewLine = "\r\n";
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index 728efa788..cc35efb3f 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -20,6 +22,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger = logger;
}
+ /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
@@ -55,11 +58,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
subEvent.StartPositionTicks = GetTicks(time[0]);
- var endTime = time[1];
- var idx = endTime.IndexOf(" ", StringComparison.Ordinal);
+ var endTime = time[1].AsSpan();
+ var idx = endTime.IndexOf(' ');
if (idx > 0)
{
- endTime = endTime.Substring(0, idx);
+ endTime = endTime.Slice(0, idx);
}
subEvent.EndPositionTicks = GetTicks(endTime);
@@ -88,7 +91,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return trackInfo;
}
- long GetTicks(string time)
+ private long GetTicks(ReadOnlySpan<char> time)
{
return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span)
? span.Ticks
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
index 45b317b2e..143c010b7 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
@@ -8,8 +8,12 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// SRT subtitle writer.
+ /// </summary>
public class SrtWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index 77033c6b4..bd330f568 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -12,6 +12,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// </summary>
public class SsaParser : ISubtitleParser
{
+ /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
{
var trackInfo = new SubtitleTrackInfo();
@@ -45,7 +46,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
header.AppendLine(line);
}
- if (line.Trim().ToLowerInvariant() == "[events]")
+ if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase))
{
eventsStarted = true;
}
@@ -63,27 +64,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles
format = line.ToLowerInvariant().Substring(8).Split(',');
for (int i = 0; i < format.Length; i++)
{
- if (format[i].Trim().ToLowerInvariant() == "layer")
+ if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase))
{
indexLayer = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "start")
+ else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase))
{
indexStart = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "end")
+ else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase))
{
indexEnd = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "text")
+ else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase))
{
indexText = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "effect")
+ else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase))
{
indexEffect = i;
}
- else if (format[i].Trim().ToLowerInvariant() == "style")
+ else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase))
{
indexStyle = i;
}
@@ -184,12 +185,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
int.Parse(timeCode[3]) * 10).Ticks;
}
- public static string GetFormattedText(string text)
+ private static string GetFormattedText(string text)
{
text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
- bool italic = false;
-
for (int i = 0; i < 10; i++) // just look ten times...
{
if (text.Contains(@"{\fn"))
@@ -200,7 +199,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string fontName = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontName, ref extraTags, out italic);
+ CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic);
text = text.Remove(start, end - start + 1);
if (italic)
{
@@ -231,7 +230,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string fontSize = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontSize, ref extraTags, out italic);
+ CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic);
if (IsInteger(fontSize))
{
text = text.Remove(start, end - start + 1);
@@ -265,7 +264,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string color = text.Substring(start + 4, end - (start + 4));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out italic);
+ CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
@@ -303,7 +302,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
string color = text.Substring(start + 5, end - (start + 5));
string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out italic);
+ CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
color = color.Replace("&", string.Empty).TrimStart('H');
color = color.PadLeft(6, '0');
@@ -354,14 +353,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
private static bool IsInteger(string s)
- {
- if (int.TryParse(s, out var i))
- {
- return true;
- }
-
- return false;
- }
+ => int.TryParse(s, out _);
private static int CountTagInText(string text, string tag)
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index f1aa8ea5f..2afa89cda 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -172,7 +174,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
inputFiles = new[] { mediaSource.Path };
}
- var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
+ var protocol = mediaSource.Protocol;
+ if (subtitleStream.IsExternal)
+ {
+ protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
+ }
+
+ var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
@@ -365,7 +373,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string filename)
{
- return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
+ return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
}
/// <summary>
@@ -426,7 +434,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
// FFmpeg automatically convert character encoding when it is UTF-16
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
- if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE"))
+ if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) &&
+ (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
+ encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
{
encodingParam = "";
}
@@ -721,19 +731,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
- var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.Slice(0, 1);
- return Path.Combine(SubtitleCachePath, prefix, filename);
+ return Path.Join(SubtitleCachePath, prefix, filename);
}
else
{
- var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.Slice(0, 1);
- return Path.Combine(SubtitleCachePath, prefix, filename);
+ return Path.Join(SubtitleCachePath, prefix, filename);
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
index 7d3e18578..e5c785bc5 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
@@ -6,8 +6,12 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// TTML subtitle writer.
+ /// </summary>
public class TtmlWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
// Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
@@ -36,9 +40,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase);
- writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
+ writer.WriteLine(
+ "<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
trackEvent.StartPositionTicks,
- (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks),
+ trackEvent.EndPositionTicks - trackEvent.StartPositionTicks,
text);
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index de35acbba..ad32cb794 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
writer.WriteLine(text);
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
}
}
}
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 1f7fa76ad..53e4540cb 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Dlna
{
diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
index 7e5c5be3b..1f7fe3030 100644
--- a/MediaBrowser.Model/Entities/DisplayPreferences.cs
+++ b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
@@ -1,22 +1,18 @@
#nullable disable
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Defines the display preferences for any item that supports them (usually Folders).
/// </summary>
- public class DisplayPreferences
+ public class DisplayPreferencesDto
{
/// <summary>
- /// The image scale.
+ /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
/// </summary>
- private const double ImageScale = .9;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
- /// </summary>
- public DisplayPreferences()
+ public DisplayPreferencesDto()
{
RememberIndexing = false;
PrimaryImageHeight = 250;
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 7a488005e..1b37cfc93 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities
{
get
{
- if (Type == MediaStreamType.Audio)
+ switch (Type)
{
- // if (!string.IsNullOrEmpty(Title))
- //{
- // return AddLanguageIfNeeded(Title);
- //}
-
- var attributes = new List<string>();
-
- if (!string.IsNullOrEmpty(Language))
- {
- attributes.Add(StringHelper.FirstToUpper(Language));
- }
-
- if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
- {
- attributes.Add(AudioCodec.GetFriendlyName(Codec));
- }
- else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
- {
- attributes.Add(Profile);
- }
-
- if (!string.IsNullOrEmpty(ChannelLayout))
- {
- attributes.Add(ChannelLayout);
- }
- else if (Channels.HasValue)
+ case MediaStreamType.Audio:
{
- attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+ var attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+ string fullLanguage = CultureInfo
+ .GetCultures(CultureTypes.NeutralCultures)
+ .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+ ?.DisplayName;
+ attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+ }
+
+ if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
+ {
+ attributes.Add(AudioCodec.GetFriendlyName(Codec));
+ }
+ else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
+ {
+ attributes.Add(Profile);
+ }
+
+ if (!string.IsNullOrEmpty(ChannelLayout))
+ {
+ attributes.Add(StringHelper.FirstToUpper(ChannelLayout));
+ }
+ else if (Channels.HasValue)
+ {
+ attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+ }
+
+ if (IsDefault)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" - ", attributes);
}
- if (IsDefault)
+ case MediaStreamType.Video:
{
- attributes.Add("Default");
+ var attributes = new List<string>();
+
+ var resolutionText = GetResolutionText();
+
+ if (!string.IsNullOrEmpty(resolutionText))
+ {
+ attributes.Add(resolutionText);
+ }
+
+ if (!string.IsNullOrEmpty(Codec))
+ {
+ attributes.Add(Codec.ToUpperInvariant());
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" ", attributes);
}
- return string.Join(" ", attributes);
- }
-
- if (Type == MediaStreamType.Video)
- {
- var attributes = new List<string>();
-
- var resolutionText = GetResolutionText();
-
- if (!string.IsNullOrEmpty(resolutionText))
+ case MediaStreamType.Subtitle:
{
- attributes.Add(resolutionText);
+ var attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+ string fullLanguage = CultureInfo
+ .GetCultures(CultureTypes.NeutralCultures)
+ .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+ ?.DisplayName;
+ attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+ }
+ else
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+ }
+
+ if (IsDefault)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ }
+
+ if (IsForced)
+ {
+ attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+ }
+
+ if (!string.IsNullOrEmpty(Title))
+ {
+ var result = new StringBuilder(Title);
+ foreach (var tag in attributes)
+ {
+ // Keep Tags that are not already in Title.
+ if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ result.Append(" - ").Append(tag);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ return string.Join(" - ", attributes.ToArray());
}
- if (!string.IsNullOrEmpty(Codec))
- {
- attributes.Add(Codec.ToUpperInvariant());
- }
-
- return string.Join(" ", attributes);
+ default:
+ return null;
}
-
- if (Type == MediaStreamType.Subtitle)
- {
-
- var attributes = new List<string>();
-
- if (!string.IsNullOrEmpty(Language))
- {
- attributes.Add(StringHelper.FirstToUpper(Language));
- }
- else
- {
- attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
- }
-
- if (IsDefault)
- {
- attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
- }
-
- if (IsForced)
- {
- attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
- }
-
- if (!string.IsNullOrEmpty(Title))
- {
- return attributes.AsEnumerable()
- // keep Tags that are not already in Title
- .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
- // attributes concatenation, starting with Title
- .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr))
- .ToString();
- }
-
- return string.Join(" - ", attributes.ToArray());
- }
-
- if (Type == MediaStreamType.Video)
- {
- }
-
- return null;
}
}
diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/MediaBrowser.Model/Entities/ScrollDirection.cs
deleted file mode 100644
index a1de0edcb..000000000
--- a/MediaBrowser.Model/Entities/ScrollDirection.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Enum ScrollDirection.
- /// </summary>
- public enum ScrollDirection
- {
- /// <summary>
- /// The horizontal.
- /// </summary>
- Horizontal,
-
- /// <summary>
- /// The vertical.
- /// </summary>
- Vertical
- }
-}
diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs
deleted file mode 100644
index f3abc06f3..000000000
--- a/MediaBrowser.Model/Entities/SortOrder.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Enum SortOrder.
- /// </summary>
- public enum SortOrder
- {
- /// <summary>
- /// The ascending.
- /// </summary>
- Ascending,
-
- /// <summary>
- /// The descending.
- /// </summary>
- Descending
- }
-}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index 2b2377fda..ab74aff28 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -2,7 +2,7 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
index b899a464b..dae885775 100644
--- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-using MediaBrowser.Model.Entities;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{
diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs
index ea363d9b1..58aecb3d3 100644
--- a/MediaBrowser.Model/Notifications/NotificationOption.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOption.cs
@@ -1,3 +1,4 @@
+#pragma warning disable CA1819 // Properties should not return arrays
#pragma warning disable CS1591
using System;
@@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
public NotificationOption(string type)
{
Type = type;
+ DisabledServices = Array.Empty<string>();
+ DisabledMonitorUsers = Array.Empty<string>();
+ SendToUsers = Array.Empty<string>();
+ }
+ public NotificationOption()
+ {
DisabledServices = Array.Empty<string>();
DisabledMonitorUsers = Array.Empty<string>();
SendToUsers = Array.Empty<string>();
}
- public string Type { get; set; }
+ public string? Type { get; set; }
/// <summary>
- /// User Ids to not monitor (it's opt out).
+ /// Gets or sets user Ids to not monitor (it's opt out).
/// </summary>
public string[] DisabledMonitorUsers { get; set; }
/// <summary>
- /// User Ids to send to (if SendToUserMode == Custom)
+ /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
/// </summary>
public string[] SendToUsers { get; set; }
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 4ee065cd1..9170c7002 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -265,7 +265,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- _logger.LogError(ex, "{0} failed in GetImageInfos for type {1}", provider.GetType().Name, item.GetType().Name);
+ _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return new List<RemoteImageInfo>();
}
}
@@ -430,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- _logger.LogError(ex, "{0} failed in Supports for type {1}", provider.GetType().Name, item.GetType().Name);
+ _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
return false;
}
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 42c7cec53..09879997a 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,7 +19,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
- <PackageReference Include="PlaylistsNET" Version="1.0.6" />
+ <PackageReference Include="PlaylistsNET" Version="1.1.2" />
<PackageReference Include="TvDbSharper" Version="3.2.0" />
</ItemGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 80acb2c05..f69ec9744 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.MediaInfo
private string GetAudioImagePath(Audio item)
{
- string filename = null;
+ string filename;
if (item.GetType() == typeof(Audio))
{
@@ -116,9 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo
filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg";
}
- var prefix = filename.Substring(0, 1);
+ var prefix = filename.AsSpan().Slice(0, 1);
- return Path.Combine(AudioImagesPath, prefix, filename);
+ return Path.Join(AudioImagesPath, prefix, filename);
}
public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
index fbf413f2b..82f26a8f2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
@@ -28,29 +28,31 @@
pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8"
};
- $('.configPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- $('#enable').checked = config.Enable;
- $('#replaceAlbumName').checked = config.ReplaceAlbumName;
-
- Dashboard.hideLoadingMsg();
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#enable').checked = config.Enable;
+ document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName;
+
+ Dashboard.hideLoadingMsg();
+ });
});
- });
-
- $('.configForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
- var form = this;
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- config.Enable = $('#enable', form).checked;
- config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
-
- ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.Enable = document.querySelector('#enable').checked;
+ config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked;
+
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index cb7c0362e..54054d015 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public override string Description => "Get artist and album metadata or images from AudioDB.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
index 90196b046..1945e6cb4 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
@@ -36,33 +36,47 @@
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
};
- $('.musicBrainzConfigPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
- $('#server').val(config.Server).change();
- $('#rateLimit').val(config.RateLimit).change();
- $('#enable').checked = config.Enable;
- $('#replaceArtistName').checked = config.ReplaceArtistName;
+ document.querySelector('.musicBrainzConfigPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+ var server = document.querySelector('#server');
+ server.value = config.Server;
+ server.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ var rateLimit = document.querySelector('#rateLimit');
+ rateLimit.value = config.RateLimit;
+ rateLimit.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ document.querySelector('#enable').checked = config.Enable;
+ document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
- Dashboard.hideLoadingMsg();
+ Dashboard.hideLoadingMsg();
+ });
});
- });
-
- $('.musicBrainzConfigForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
-
- var form = this;
- ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
- config.Server = $('#server', form).val();
- config.RateLimit = $('#rateLimit', form).val();
- config.Enable = $('#enable', form).checked;
- config.ReplaceArtistName = $('#replaceArtistName', form).checked;
-
- ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+
+ document.querySelector('.musicBrainzConfigForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+ config.Server = document.querySelector('#server').value;
+ config.RateLimit = document.querySelector('#rateLimit').value;
+ config.Enable = document.querySelector('#enable').checked;
+ config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked;
+
+ ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index a7e6267da..90266e440 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -23,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public const long DefaultRateLimit = 2000u;
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
index 8b117ec8d..f4375b3cb 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
@@ -24,25 +24,28 @@
pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
};
- $('.configPage').on('pageshow', function () {
- Dashboard.showLoadingMsg();
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- $('#castAndCrew').checked = config.CastAndCrew;
- Dashboard.hideLoadingMsg();
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#castAndCrew').checked = config.CastAndCrew;
+ Dashboard.hideLoadingMsg();
+ });
});
- });
- $('.configForm').on('submit', function (e) {
- Dashboard.showLoadingMsg();
-
- var form = this;
- ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
- config.CastAndCrew = $('#castAndCrew', form).checked;
- ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.CastAndCrew = document.querySelector('#castAndCrew').checked;
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
});
-
- return false;
- });
</script>
</div>
</body>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 944ba26af..12aecba84 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
if (result.Year.Length > 0
- && int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
+ && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
{
item.ProductionYear = parsedYear;
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 9700f3b18..13098d140 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index 4cf5f4ce6..41ca56164 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public override string Description => "Get metadata for movies and other video content from OMDb.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
index aa5f819f0..e7079ed3c 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
@@ -17,6 +17,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
public override string Description => "Get metadata for movies and other video content from TheTVDB.";
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
+
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index ced287d54..52fc53872 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -188,7 +188,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var i = 0; i < episode.GuestStars.Length; ++i)
{
var currentActor = episode.GuestStars[i];
- var roleStartIndex = currentActor.IndexOf('(');
+ var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
if (roleStartIndex == -1)
{
@@ -207,7 +207,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var j = i + 1; j < episode.GuestStars.Length; ++j)
{
var currentRole = episode.GuestStars[j];
- var roleEndIndex = currentRole.IndexOf(')');
+ var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
if (roleEndIndex == -1)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
index 7566df8b6..704ebcd5a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public BelongsToCollection Belongs_To_Collection { get; set; }
- public int Budget { get; set; }
+ public long Budget { get; set; }
public List<Genre> Genres { get; set; }
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public string Release_Date { get; set; }
- public int Revenue { get; set; }
+ public long Revenue { get; set; }
public int Runtime { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 76d3f8224..58cbf9eef 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -251,9 +251,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
{
- var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
+ var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
- return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
+ return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
}
internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 4181d37ef..a2c0e62c1 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in DummySeasonProvider");
+ Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index b2d99c1a1..b06464409 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -222,8 +222,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
- var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
- if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+ index = tmdbId.IndexOf('-');
+ if (index != -1)
+ {
+ tmdbId = tmdbId.Slice(0, index);
+ }
+
+ if (!tmdbId.IsEmpty
+ && !tmdbId.IsWhiteSpace()
+ && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
}
@@ -237,8 +245,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
- var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
- if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
+ if (!tvdbId.IsEmpty
+ && !tvdbId.IsWhiteSpace()
+ && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
}
@@ -442,8 +452,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasAspectRatio = item as IHasAspectRatio;
- if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
+ if (!string.IsNullOrWhiteSpace(val)
+ && item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
}
diff --git a/README.md b/README.md
index 83090100d..3f1b5e514 100644
--- a/README.md
+++ b/README.md
@@ -162,6 +162,7 @@ The following sections describe some more advanced scenarios for running the ser
It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
-To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
+To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
+switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
diff --git a/SharedVersion.cs b/SharedVersion.cs
index 6981c1ca9..d17074fc0 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.6.0")]
-[assembly: AssemblyFileVersion("10.6.0")]
+[assembly: AssemblyVersion("10.7.0")]
+[assembly: AssemblyFileVersion("10.7.0")]
diff --git a/build.yaml b/build.yaml
index 9e590e5a0..7c32d955c 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
-version: "10.6.0"
+version: "10.7.0"
packages:
- debian.amd64
- debian.arm64
diff --git a/bump_version b/bump_version
index 46b7f86e0..1c943f691 100755
--- a/bump_version
+++ b/bump_version
@@ -4,6 +4,7 @@
set -o errexit
set -o pipefail
+set -o xtrace
usage() {
echo -e "bump_version - increase the shared version and generate changelogs"
@@ -58,7 +59,7 @@ sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
-echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
diff --git a/debian/changelog b/debian/changelog
index d38d03805..184143fe9 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+jellyfin-server (10.7.0-1) unstable; urgency=medium
+
+ * Forthcoming stable release
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400
+
jellyfin-server (10.6.0-2) unstable; urgency=medium
* Fix upgrade bug
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 64c98520c..7cbfa88ee 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -31,7 +31,7 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
#JELLYFIN_SERVICE_OPT="--service"
# [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
#
# SysV init/Upstart options
@@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"
diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin
index 9a41eafae..026fcb821 100644
--- a/debian/metapackage/jellyfin
+++ b/debian/metapackage/jellyfin
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2
Package: jellyfin
-Version: 10.6.0
+Version: 10.7.0
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 9311864a6..b0a729a9e 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -7,7 +7,7 @@
%endif
Name: jellyfin
-Version: 10.6.0
+Version: 10.7.0
Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv3
@@ -139,6 +139,8 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
+- Forthcoming stable release
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index c145ddc9d..b4e6db8f3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -9,17 +9,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/small.jpg", true)]
[InlineData("/media/albumart.jpg", true)]
[InlineData("/media/movie.sample.mp4", true)]
+ [InlineData("/media/movie/sample.mp4", true)]
+ [InlineData("/media/movie/sample/movie.mp4", true)]
+ [InlineData("/foo/sample/bar/baz.mkv", false)]
+ [InlineData("/media/movies/the sample/the sample.mkv", false)]
+ [InlineData("/media/movies/sampler.mkv", false)]
[InlineData("/media/movies/#Recycle/test.txt", true)]
[InlineData("/media/movies/#recycle/", true)]
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
- [InlineData("/media/.hiddendir/file.mp4", true)]
+ [InlineData("/media/.hiddendir/file.mp4", false)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
+ [InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
[InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
[InlineData("/directory/@Recycle", true)]
[InlineData("/directory/@Recycle/file.mp3", true)]
+ [InlineData("/media/movies/.@__thumb", true)]
+ [InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)]
+ [InlineData("/media/music/Foo B.A.R./epic.flac", false)]
+ [InlineData("/media/music/Foo B.A.R", false)]
+ // This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78
+ // [InlineData("/media/music/Foo B.A.R.", false)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));