Explorar el Código

Merge branch 'master' into network-rewrite

Shadowghost hace 2 años
padre
commit
d8d5c86d49
Se han modificado 32 ficheros con 251 adiciones y 90 borrados
  1. 4 4
      .github/workflows/codeql-analysis.yml
  2. 2 2
      .github/workflows/openapi.yml
  3. 1 0
      CONTRIBUTORS.md
  4. 8 8
      Directory.Packages.props
  5. 33 6
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  6. 1 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  7. 10 2
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  8. 1 0
      Emby.Server.Implementations/Dto/DtoService.cs
  9. 7 4
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  10. 6 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  11. 15 5
      Emby.Server.Implementations/Library/UserViewManager.cs
  12. 2 2
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  13. 1 1
      Emby.Server.Implementations/Localization/Core/bn.json
  14. 4 2
      Emby.Server.Implementations/Localization/Core/lv.json
  15. 3 1
      Emby.Server.Implementations/Localization/Core/ml.json
  16. 20 1
      Emby.Server.Implementations/Localization/Core/te.json
  17. 2 1
      Emby.Server.Implementations/Localization/Core/tr.json
  18. 4 23
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  19. 0 6
      Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
  20. 10 2
      Jellyfin.Api/Controllers/ItemUpdateController.cs
  21. 5 2
      Jellyfin.Api/Controllers/ItemsController.cs
  22. 3 3
      Jellyfin.Api/Controllers/PlaylistsController.cs
  23. 0 2
      Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
  24. 15 6
      Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
  25. 7 1
      Jellyfin.Server/Startup.cs
  26. 7 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  27. 3 4
      MediaBrowser.Controller/Playlists/IPlaylistManager.cs
  28. 8 0
      MediaBrowser.Controller/Playlists/Playlist.cs
  29. 2 0
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  30. 6 0
      MediaBrowser.Model/Dto/BaseItemDto.cs
  31. 60 0
      MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
  32. 1 1
      MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

+ 4 - 4
.github/workflows/codeql-analysis.yml

@@ -22,16 +22,16 @@ jobs:
     - name: Checkout repository
       uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
     - name: Setup .NET
-      uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
+      uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
       with:
         dotnet-version: '7.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
+      uses: github/codeql-action/init@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
+      uses: github/codeql-action/autobuild@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
+      uses: github/codeql-action/analyze@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4

+ 2 - 2
.github/workflows/openapi.yml

@@ -19,7 +19,7 @@ jobs:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Setup .NET
-        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
+        uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
         with:
           dotnet-version: '7.0.x'
       - name: Generate openapi.json
@@ -51,7 +51,7 @@ jobs:
           ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
           git checkout --progress --force $ANCESTOR_REF
       - name: Setup .NET
-        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
+        uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
         with:
           dotnet-version: '7.0.x'
       - name: Generate openapi.json

+ 1 - 0
CONTRIBUTORS.md

@@ -126,6 +126,7 @@
  - [SuperSandro2000](https://github.com/SuperSandro2000)
  - [tbraeutigam](https://github.com/tbraeutigam)
  - [teacupx](https://github.com/teacupx)
+ - [TelepathicWalrus](https://github.com/TelepathicWalrus)
  - [Terror-Gene](https://github.com/Terror-Gene)
  - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
  - [ThibaultNocchi](https://github.com/ThibaultNocchi)

+ 8 - 8
Directory.Packages.props

@@ -13,15 +13,15 @@
     <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
     <PackageVersion Include="BlurHashSharp" Version="1.2.0" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
-    <PackageVersion Include="coverlet.collector" Version="3.2.0" />
+    <PackageVersion Include="coverlet.collector" Version="6.0.0" />
     <PackageVersion Include="Diacritics" Version="3.3.18" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" />
     <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
     <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
-    <PackageVersion Include="libse" Version="3.6.11" />
-    <PackageVersion Include="LrcParser" Version="2023.308.0" />
+    <PackageVersion Include="libse" Version="3.6.13" />
+    <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
     <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
@@ -46,20 +46,20 @@
     <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
-    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
     <PackageVersion Include="MimeTypes" Version="2.4.0" />
     <PackageVersion Include="Mono.Nat" Version="3.0.4" />
     <PackageVersion Include="Moq" Version="4.18.4" />
     <PackageVersion Include="NEbml" Version="0.11.0" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
-    <PackageVersion Include="PlaylistsNET" Version="1.3.1" />
+    <PackageVersion Include="PlaylistsNET" Version="1.3.2" />
     <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
     <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
     <PackageVersion Include="prometheus-net" Version="8.0.0" />
-    <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
+    <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
     <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
-    <PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
+    <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" />
     <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
     <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
     <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
@@ -71,7 +71,7 @@
     <PackageVersion Include="SkiaSharp" Version="2.88.3" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
-    <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
+    <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
     <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />

+ 33 - 6
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -2,9 +2,11 @@
 
 using System;
 using System.Globalization;
+using System.IO;
 using System.Net.Http;
 using System.Net.Mime;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
@@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class DlnaHttpClient
+    /// <summary>
+    /// Http client for Dlna PlayTo function.
+    /// </summary>
+    public partial class DlnaHttpClient
     {
         private readonly ILogger _logger;
         private readonly IHttpClientFactory _httpClientFactory;
@@ -54,15 +59,30 @@ namespace Emby.Dlna.PlayTo
                     LoadOptions.None,
                     cancellationToken).ConfigureAwait(false);
             }
-            catch (XmlException ex)
+            catch (XmlException)
             {
-                _logger.LogError(ex, "Failed to parse response");
-                if (_logger.IsEnabled(LogLevel.Debug))
+                // try correcting the Xml response with common errors
+                var xmlString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+
+                // find and replace unescaped ampersands (&)
+                xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
+
+                try
                 {
-                    _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+                    // retry reading Xml
+                    var xmlReader = new StringReader(xmlString);
+                    return await XDocument.LoadAsync(
+                        xmlReader,
+                        LoadOptions.None,
+                        cancellationToken).ConfigureAwait(false);
                 }
+                catch (XmlException ex)
+                {
+                    _logger.LogError(ex, "Failed to parse response");
+                    _logger.LogDebug("Malformed response: {Content}\n", xmlString);
 
-                return null;
+                    return null;
+                }
             }
         }
 
@@ -104,5 +124,12 @@ namespace Emby.Dlna.PlayTo
             // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
             return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
         }
+
+        /// <summary>
+        /// Compile-time generated regular expression for escaping ampersands.
+        /// </summary>
+        /// <returns>Compiled regular expression.</returns>
+        [GeneratedRegex("(&(?![a-z]*;))")]
+        private static partial Regex EscapeAmpersandRegex();
     }
 }

+ 1 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -14,7 +14,7 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
         {
             { HostWebClientKey, bool.TrueString },
-            { DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.FalseString },

+ 10 - 2
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
 
         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)";
+            (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,LUFS,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,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
 
         private readonly IServerConfigurationManager _config;
         private readonly IServerApplicationHost _appHost;
@@ -110,6 +110,7 @@ namespace Emby.Server.Implementations.Data
             "PrimaryVersionId",
             "DateLastMediaAdded",
             "Album",
+            "LUFS",
             "CriticRating",
             "IsVirtualItem",
             "SeriesName",
@@ -489,6 +490,7 @@ namespace Emby.Server.Implementations.Data
                         AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
+                        AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
                         AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@@ -906,6 +908,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             saveItemStatement.TryBind("@Album", item.Album);
+            saveItemStatement.TryBind("@LUFS", item.LUFS);
             saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
 
             if (item is IHasSeries hasSeriesName)
@@ -1756,6 +1759,11 @@ namespace Emby.Server.Implementations.Data
                 item.Album = album;
             }
 
+            if (reader.TryGetSingle(index++, out var lUFS))
+            {
+                item.LUFS = lUFS;
+            }
+
             if (reader.TryGetSingle(index++, out var criticRating))
             {
                 item.CriticRating = criticRating;

+ 1 - 0
Emby.Server.Implementations/Dto/DtoService.cs

@@ -906,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
             // Add audio info
             if (item is Audio audio)
             {
+                dto.LUFS = audio.LUFS;
                 dto.Album = audio.Album;
                 if (audio.ExtraType.HasValue)
                 {

+ 7 - 4
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         {
             if (args.IsDirectory)
             {
-                // It's a boxset if the path is a directory with [playlist] in it's the name
+                // It's a boxset if the path is a directory with [playlist] in its name
                 var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
                 if (string.IsNullOrEmpty(filename))
                 {
@@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                        Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
+                        OpenAccess = true
                     };
                 }
 
@@ -53,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = filename
+                        Name = filename,
+                        OpenAccess = true
                     };
                 }
             }
@@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                         Path = args.Path,
                         Name = Path.GetFileNameWithoutExtension(args.Path),
                         IsInMixedFolder = true,
-                        PlaylistMediaType = MediaType.Audio
+                        PlaylistMediaType = MediaType.Audio,
+                        OpenAccess = true
                     };
                 }
             }

+ 6 - 0
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         {
             var justName = Path.GetFileName(path.AsSpan());
 
+            var imdbId = justName.GetAttributeValue("imdbid");
+            if (!string.IsNullOrEmpty(imdbId))
+            {
+                item.SetProviderId(MetadataProvider.Imdb, imdbId);
+            }
+
             var tvdbId = justName.GetAttributeValue("tvdbid");
             if (!string.IsNullOrEmpty(tvdbId))
             {

+ 15 - 5
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library
         public Folder[] GetUserViews(UserViewQuery query)
         {
             var user = _userManager.GetUserById(query.UserId);
-
             if (user is null)
             {
-                throw new ArgumentException("User Id specified in the query does not exist.", nameof(query));
+                throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
             }
 
             var folders = _libraryManager.GetUserRootFolder()
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
                 .ToList();
 
             var groupedFolders = new List<ICollectionFolder>();
-
             var list = new List<Folder>();
 
             foreach (var folder in folders)
@@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
                 var collectionFolder = folder as ICollectionFolder;
                 var folderViewType = collectionFolder?.CollectionType;
 
+                // Playlist library requires special handling because the folder only refrences user playlists
+                if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+                {
+                    var items = folder.GetItemList(new InternalItemsQuery(user)
+                    {
+                        ParentId = folder.ParentId
+                    });
+
+                    if (!items.Any(item => item.IsVisible(user)))
+                    {
+                        continue;
+                    }
+                }
+
                 if (UserView.IsUserSpecific(folder))
                 {
                     list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
@@ -132,14 +144,12 @@ namespace Emby.Server.Implementations.Library
             }
 
             var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
-
             var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
 
             return list
                 .OrderBy(i =>
                 {
                     var index = Array.IndexOf(orders, i.Id);
-
                     if (index == -1
                         && i is UserView view
                         && !view.DisplayParentId.Equals(default))

+ 2 - 2
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -462,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
 
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
-            foreach (ReadOnlySpan<char> i in programIds)
+            foreach (var i in programIds)
             {
                 str.Append('"')
-                    .Append(i.Slice(0, 10))
+                    .Append(i[..10])
                     .Append("\",");
             }
 

+ 1 - 1
Emby.Server.Implementations/Localization/Core/bn.json

@@ -117,7 +117,7 @@
     "Forced": "জোরকরে",
     "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
     "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
-    "Default": "প্রাথমিক",
+    "Default": "ডিফল্ট",
     "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
     "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
     "External": "বাহ্যিক",

+ 4 - 2
Emby.Server.Implementations/Localization/Core/lv.json

@@ -84,7 +84,7 @@
     "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
     "Books": "Grāmatas",
     "Artists": "Izpildītāji",
-    "Albums": "Albūmi",
+    "Albums": "Albumi",
     "ProviderValue": "Provider: {0}",
     "HeaderFavoriteSongs": "Dziesmu Favorīti",
     "HeaderFavoriteShows": "Raidījumu Favorīti",
@@ -121,5 +121,7 @@
     "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
     "TaskOptimizeDatabase": "Optimizēt datubāzi",
     "External": "Ārējais",
-    "HearingImpaired": "Ar dzirdes traucējumiem"
+    "HearingImpaired": "Ar dzirdes traucējumiem",
+    "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+    "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/ml.json

@@ -119,5 +119,7 @@
     "Genres": "വിഭാഗങ്ങൾ",
     "Channels": "ചാനലുകൾ",
     "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
-    "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക"
+    "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
+    "HearingImpaired": "കേൾവി തകരാറുകൾ",
+    "External": "പുറമേയുള്ള"
 }

+ 20 - 1
Emby.Server.Implementations/Localization/Core/te.json

@@ -19,5 +19,24 @@
     "Channels": "ఛానెల్‌లు",
     "Books": "పుస్తకాలు",
     "Artists": "కళాకారులు",
-    "Albums": "ఆల్బమ్‌లు"
+    "Albums": "ఆల్బమ్‌లు",
+    "HearingImpaired": "వినికిడి లోపం",
+    "HomeVideos": "హోమ్ వీడియోలు",
+    "AppDeviceValues": "అప్లికేషన్ : {0}, పరికరం: {1}",
+    "Application": "అప్లికేషన్",
+    "AuthenticationSucceededWithUserName": "విజయవంతంగా ఆమోదించబడింది",
+    "CameraImageUploadedFrom": "{0} నుండి కొత్త కెమెరా చిత్రం అప్‌లోడ్ చేయబడింది",
+    "ChapterNameValue": "అధ్యాయం",
+    "DeviceOfflineWithName": "{0} డిస్‌కనెక్ట్ చేయబడింది",
+    "DeviceOnlineWithName": "{0} కనెక్ట్ చేయబడింది",
+    "External": "బాహ్య",
+    "FailedLoginAttemptWithUserName": "{0} నుండి విఫలమైన లాగిన్ ప్రయత్నం",
+    "HeaderFavoriteAlbums": "ఇష్టమైన ఆల్బమ్‌లు",
+    "HeaderFavoriteArtists": "ఇష్టమైన కళాకారులు",
+    "HeaderFavoriteEpisodes": "ఇష్టమైన ఎపిసోడ్‌లు",
+    "HeaderFavoriteShows": "ఇష్టమైన ప్రదర్శనలు",
+    "HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
+    "HeaderLiveTV": "ప్రత్యక్ష TV",
+    "HeaderNextUp": "తదుపరి",
+    "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/tr.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Veritabanını optimize et",
     "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.",
     "TaskKeyframeExtractor": "Kare Ayırt Edici",
-    "External": "Harici"
+    "External": "Harici",
+    "HearingImpaired": "Duyma engelli"
 }

+ 4 - 23
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -67,9 +67,8 @@ namespace Emby.Server.Implementations.Playlists
         public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
         {
             var name = options.Name;
-
             var folderName = _fileSystem.GetValidFilename(name);
-            var parentFolder = GetPlaylistsFolder(Guid.Empty);
+            var parentFolder = GetPlaylistsFolder(options.UserId);
             if (parentFolder is null)
             {
                 throw new ArgumentException(nameof(parentFolder));
@@ -80,7 +79,6 @@ namespace Emby.Server.Implementations.Playlists
                 foreach (var itemId in options.ItemIdList)
                 {
                     var item = _libraryManager.GetItemById(itemId);
-
                     if (item is null)
                     {
                         throw new ArgumentException("No item exists with the supplied Id");
@@ -121,7 +119,6 @@ namespace Emby.Server.Implementations.Playlists
             }
 
             var user = _userManager.GetUserById(options.UserId);
-
             var path = Path.Combine(parentFolder.Path, folderName);
             path = GetTargetPath(path);
 
@@ -130,7 +127,6 @@ namespace Emby.Server.Implementations.Playlists
             try
             {
                 Directory.CreateDirectory(path);
-
                 var playlist = new Playlist
                 {
                     Name = name,
@@ -140,7 +136,6 @@ namespace Emby.Server.Implementations.Playlists
                 };
 
                 playlist.SetMediaType(options.MediaType);
-
                 parentFolder.AddChild(playlist);
 
                 await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
@@ -326,7 +321,8 @@ namespace Emby.Server.Implementations.Playlists
             }
         }
 
-        private void SavePlaylistFile(Playlist item)
+        /// <inheritdoc />
+        public void SavePlaylistFile(Playlist item)
         {
             // this is probably best done as a metadata provider
             // saving a file over itself will require some work to prevent this from happening when not needed
@@ -549,7 +545,7 @@ namespace Emby.Server.Implementations.Playlists
                         SavePlaylistFile(playlist);
                     }
                 }
-                else
+                else if (!playlist.OpenAccess)
                 {
                     // Remove playlist if not shared
                     _libraryManager.DeleteItem(
@@ -564,20 +560,5 @@ namespace Emby.Server.Implementations.Playlists
                 }
             }
         }
-
-        /// <inheritdoc />
-        public async Task UpdatePlaylistAsync(Playlist playlist)
-        {
-            var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id);
-            currentPlaylist.OwnerUserId = playlist.OwnerUserId;
-            currentPlaylist.Shares = playlist.Shares;
-
-            await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
-            if (currentPlaylist.IsFile)
-            {
-                SavePlaylistFile(currentPlaylist);
-            }
-        }
     }
 }

+ 0 - 6
Emby.Server.Implementations/Playlists/PlaylistsFolder.cs

@@ -27,11 +27,6 @@ namespace Emby.Server.Implementations.Playlists
         [JsonIgnore]
         public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
 
-        public override bool IsVisible(User user)
-        {
-            return base.IsVisible(user) && GetChildren(user, true).Any();
-        }
-
         protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
         {
             return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
@@ -47,7 +42,6 @@ namespace Emby.Server.Implementations.Playlists
 
             query.Recursive = true;
             query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
-            query.Parent = null;
             return LibraryManager.GetItemsResult(query);
         }
 

+ 10 - 2
Jellyfin.Api/Controllers/ItemUpdateController.cs

@@ -251,8 +251,6 @@ public class ItemUpdateController : BaseJellyfinApiController
             channel.Height = request.Height.Value;
         }
 
-        item.Tags = request.Tags;
-
         if (request.Taglines is not null)
         {
             item.Tagline = request.Taglines.FirstOrDefault();
@@ -276,12 +274,19 @@ public class ItemUpdateController : BaseJellyfinApiController
         item.OfficialRating = request.OfficialRating;
         item.CustomRating = request.CustomRating;
 
+        var currentTags = item.Tags;
+        var newTags = request.Tags;
+        var removedTags = currentTags.Except(newTags).ToList();
+        var addedTags = newTags.Except(currentTags).ToList();
+        item.Tags = newTags;
+
         if (item is Series rseries)
         {
             foreach (Season season in rseries.Children)
             {
                 season.OfficialRating = request.OfficialRating;
                 season.CustomRating = request.CustomRating;
+                season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
                 season.OnMetadataChanged();
                 await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
@@ -289,6 +294,7 @@ public class ItemUpdateController : BaseJellyfinApiController
                 {
                     ep.OfficialRating = request.OfficialRating;
                     ep.CustomRating = request.CustomRating;
+                    ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
                     ep.OnMetadataChanged();
                     await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
                 }
@@ -300,6 +306,7 @@ public class ItemUpdateController : BaseJellyfinApiController
             {
                 ep.OfficialRating = request.OfficialRating;
                 ep.CustomRating = request.CustomRating;
+                ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
                 ep.OnMetadataChanged();
                 await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             }
@@ -310,6 +317,7 @@ public class ItemUpdateController : BaseJellyfinApiController
             {
                 track.OfficialRating = request.OfficialRating;
                 track.CustomRating = request.CustomRating;
+                track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
                 track.OnMetadataChanged();
                 await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             }

+ 5 - 2
Jellyfin.Api/Controllers/ItemsController.cs

@@ -503,6 +503,7 @@ public class ItemsController : BaseJellyfinApiController
                 }
             }
 
+            query.Parent = null;
             result = folder.GetItems(query);
         }
         else
@@ -511,10 +512,12 @@ public class ItemsController : BaseJellyfinApiController
             result = new QueryResult<BaseItem>(itemsArray);
         }
 
+        // result might include items not accessible by the user, DtoService will remove them
+        var accessibleItems = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
         return new QueryResult<BaseItemDto>(
             startIndex,
-            result.TotalRecordCount,
-            _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+            accessibleItems.Count,
+            accessibleItems);
     }
 
     /// <summary>

+ 3 - 3
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -64,6 +64,7 @@ public class PlaylistsController : BaseJellyfinApiController
     /// <param name="userId">The user id.</param>
     /// <param name="mediaType">The media type.</param>
     /// <param name="createPlaylistRequest">The create playlist payload.</param>
+    /// <response code="200">Playlist created.</response>
     /// <returns>
     /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
     /// The task result contains an <see cref="OkResult"/> indicating success.
@@ -167,6 +168,8 @@ public class PlaylistsController : BaseJellyfinApiController
     /// <response code="404">Playlist not found.</response>
     /// <returns>The original playlist items.</returns>
     [HttpGet("{playlistId}/Items")]
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
         [FromRoute, Required] Guid playlistId,
         [FromQuery, Required] Guid userId,
@@ -189,9 +192,7 @@ public class PlaylistsController : BaseJellyfinApiController
             : _userManager.GetUserById(userId);
 
         var items = playlist.GetManageableItems().ToArray();
-
         var count = items.Length;
-
         if (startIndex.HasValue)
         {
             items = items.Skip(startIndex.Value).ToArray();
@@ -207,7 +208,6 @@ public class PlaylistsController : BaseJellyfinApiController
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
         var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
-
         for (int index = 0; index < dtos.Count; index++)
         {
             dtos[index].PlaylistItemId = items[index].Item1.Id;

+ 0 - 2
Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -48,8 +48,6 @@ public class BaseUrlRedirectionMiddleware
         if (string.IsNullOrEmpty(localPath)
             || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
             || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase)
             || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
            )
         {

+ 15 - 6
Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using System.Threading;
 
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
@@ -53,13 +54,21 @@ internal class FixPlaylistOwner : IMigrationRoutine
             foreach (var playlist in playlists)
             {
                 var shares = playlist.Shares;
-                var firstEditShare = shares.First(x => x.CanEdit);
-                if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid))
+                if (shares.Length > 0)
                 {
-                    playlist.OwnerUserId = guid;
-                    playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
-
-                    _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult();
+                    var firstEditShare = shares.First(x => x.CanEdit);
+                    if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid))
+                    {
+                        playlist.OwnerUserId = guid;
+                        playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
+                        playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+                        _playlistManager.SavePlaylistFile(playlist);
+                    }
+                }
+                else
+                {
+                    playlist.OpenAccess = true;
+                    playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
                 }
             }
         }

+ 7 - 1
Jellyfin.Server/Startup.cs

@@ -182,7 +182,7 @@ namespace Jellyfin.Server
 
                 // This must be injected before any path related middleware.
                 mainApp.UsePathTrim();
-                mainApp.UseStaticFiles();
+
                 if (appConfig.HostWebClient())
                 {
                     var extensionProvider = new FileExtensionContentTypeProvider();
@@ -190,6 +190,11 @@ namespace Jellyfin.Server
                     // subtitles octopus requires .data, .mem files.
                     extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet);
                     extensionProvider.Mappings.Add(".mem", MediaTypeNames.Application.Octet);
+                    mainApp.UseDefaultFiles(new DefaultFilesOptions
+                    {
+                        FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
+                        RequestPath = "/web"
+                    });
                     mainApp.UseStaticFiles(new StaticFileOptions
                     {
                         FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
@@ -200,6 +205,7 @@ namespace Jellyfin.Server
                     mainApp.UseRobotsRedirection();
                 }
 
+                mainApp.UseStaticFiles();
                 mainApp.UseAuthentication();
                 mainApp.UseJellyfinApiSwagger(_serverConfigurationManager);
                 mainApp.UseQueryStringDecoding();

+ 7 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -128,6 +128,13 @@ namespace MediaBrowser.Controller.Entities
         [JsonIgnore]
         public string Album { get; set; }
 
+        /// <summary>
+        /// Gets or sets the LUFS value.
+        /// </summary>
+        /// <value>The LUFS Value.</value>
+        [JsonIgnore]
+        public float LUFS { get; set; }
+
         /// <summary>
         /// Gets or sets the channel identifier.
         /// </summary>

+ 3 - 4
MediaBrowser.Controller/Playlists/IPlaylistManager.cs

@@ -66,10 +66,9 @@ namespace MediaBrowser.Controller.Playlists
         Task RemovePlaylistsAsync(Guid userId);
 
         /// <summary>
-        /// Updates a playlist.
+        /// Saves a playlist.
         /// </summary>
-        /// <param name="playlist">The updated playlist.</param>
-        /// <returns>Task.</returns>
-        Task UpdatePlaylistAsync(Playlist playlist);
+        /// <param name="item">The playlist.</param>
+        void SavePlaylistFile(Playlist item);
     }
 }

+ 8 - 0
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -34,10 +34,13 @@ namespace MediaBrowser.Controller.Playlists
         public Playlist()
         {
             Shares = Array.Empty<Share>();
+            OpenAccess = false;
         }
 
         public Guid OwnerUserId { get; set; }
 
+        public bool OpenAccess { get; set; }
+
         public Share[] Shares { get; set; }
 
         [JsonIgnore]
@@ -233,6 +236,11 @@ namespace MediaBrowser.Controller.Playlists
                 return base.IsVisible(user);
             }
 
+            if (OpenAccess)
+            {
+                return true;
+            }
+
             var userId = user.Id;
             if (userId.Equals(OwnerUserId))
             {

+ 2 - 0
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -30,6 +30,8 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableRealtimeMonitor { get; set; }
 
+        public bool EnableLUFSScan { get; set; }
+
         public bool EnableChapterImageExtraction { get; set; }
 
         public bool ExtractChapterImagesDuringLibraryScan { get; set; }

+ 6 - 0
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -779,6 +779,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The timer identifier.</value>
         public string TimerId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the LUFS value.
+        /// </summary>
+        /// <value>The LUFS Value.</value>
+        public float LUFS { get; set; }
+
         /// <summary>
         /// Gets or sets the current program.
         /// </summary>

+ 60 - 0
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -1,6 +1,9 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
@@ -14,6 +17,7 @@ using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
 using TagLib;
 
 namespace MediaBrowser.Providers.MediaInfo
@@ -23,6 +27,10 @@ namespace MediaBrowser.Providers.MediaInfo
     /// </summary>
     public class AudioFileProber
     {
+        // Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain).
+        private const float DefaultLUFSValue = -18;
+
+        private readonly ILogger<AudioFileProber> _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IItemRepository _itemRepo;
         private readonly ILibraryManager _libraryManager;
@@ -31,16 +39,19 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
         /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
         /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public AudioFileProber(
+            ILogger<AudioFileProber> logger,
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
             ILibraryManager libraryManager)
         {
+            _logger = logger;
             _mediaEncoder = mediaEncoder;
             _itemRepo = itemRepo;
             _libraryManager = libraryManager;
@@ -89,6 +100,54 @@ namespace MediaBrowser.Providers.MediaInfo
                 Fetch(item, result, cancellationToken);
             }
 
+            var libraryOptions = _libraryManager.GetLibraryOptions(item);
+
+            if (libraryOptions.EnableLUFSScan)
+            {
+                string output;
+                using (var process = new Process()
+                {
+                    StartInfo = new ProcessStartInfo
+                    {
+                        FileName = _mediaEncoder.EncoderPath,
+                        Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -",
+                        RedirectStandardOutput = false,
+                        RedirectStandardError = true
+                    },
+                })
+                {
+                    try
+                    {
+                        process.Start();
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Error starting ffmpeg");
+
+                        throw;
+                    }
+
+                    output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+                    cancellationToken.ThrowIfCancellationRequested();
+                    MatchCollection split = Regex.Matches(output, @"I:\s+(.*?)\s+LUFS");
+
+                    if (split.Count != 0)
+                    {
+                        item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+                    }
+                    else
+                    {
+                        item.LUFS = DefaultLUFSValue;
+                    }
+                }
+            }
+            else
+            {
+                item.LUFS = DefaultLUFSValue;
+            }
+
+            _logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS);
+
             return ItemUpdateType.MetadataImport;
         }
 
@@ -196,6 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 audio.Album = tags.Album;
                 audio.IndexNumber = Convert.ToInt32(tags.Track);
                 audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+
                 if (tags.Year != 0)
                 {
                     var year = Convert.ToInt32(tags.Year);

+ 1 - 1
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo
             NamingOptions namingOptions)
         {
             _logger = loggerFactory.CreateLogger<ProbeProvider>();
-            _audioProber = new AudioFileProber(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
+            _audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
             _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _videoProber = new FFProbeVideoInfo(