diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2023-05-25 17:07:43 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2023-05-25 17:07:43 +0200 |
| commit | d8d5c86d49cfb81baea5f7219fa495c2b7edf98c (patch) | |
| tree | 5befce0ac43b4f98ca29824641c5bf5fcc27c685 | |
| parent | b37e9209df94dcad757d0b9ad0a7a7076c3cf743 (diff) | |
| parent | d67f10ba8c858626a8c9ea39c7cc48e3aa6ff415 (diff) | |
Merge branch 'master' into network-rewrite
32 files changed, 251 insertions, 90 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8a806f941..51fb446ee 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.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 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index e970bc706..c2387f2ef 100644 --- a/.github/workflows/openapi.yml +++ b/.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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c9430b235..0b322685d 100644 --- a/CONTRIBUTORS.md +++ b/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) diff --git a/Directory.Packages.props b/Directory.Packages.props index 26a13e17e..b9a9a1b68 100644 --- a/Directory.Packages.props +++ b/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" /> diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 75ff542dd..4e9903f26 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/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, "&"); + + 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(); } } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index f0a4c8ffb..630265dac 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/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 }, diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 22d485d33..c32e7c75a 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/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; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 8fa2f0566..7a6ed2cb8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/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) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 7a2b3da3a..5d569009d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/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 }; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 8f69175d0..d4f275bed 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/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)) { diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 17f1d1905..2c3dc1857 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/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)) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index ca3e45707..7645c6c52 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/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("\","); } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 355fb3b21..005926231 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -117,7 +117,7 @@ "Forced": "জোরকরে", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", - "Default": "প্রাথমিক", + "Default": "ডিফল্ট", "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "External": "বাহ্যিক", diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index f753a369a..f7b24412a 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/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." } diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index acc7746c1..0620fbcdb 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -119,5 +119,7 @@ "Genres": "വിഭാഗങ്ങൾ", "Channels": "ചാനലുകൾ", "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", - "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക" + "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", + "HearingImpaired": "കേൾവി തകരാറുകൾ", + "External": "പുറമേയുള്ള" } diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index a9a8ceae0..24168b611 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/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": "రికార్డింగ్ గుంపులు" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index b802db982..9a140f871 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/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" } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6176879b6..702f8d45b 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/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); - } - } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index e2f2e436f..549209715 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/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); } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index ece053a9a..504f2fa1d 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/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); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 377526729..d4116116b 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/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> diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c6dbea5e2..8d2a738d4 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/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; diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 7bcc328aa..2241c68e7 100644 --- a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/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) ) { diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 55aadae79..cf3182003 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/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(); } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5b9397e30..b759b6bca 100644 --- a/Jellyfin.Server/Startup.cs +++ b/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(); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index adc7b2f95..1e868194e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -129,6 +129,13 @@ namespace MediaBrowser.Controller.Entities 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> /// <value>The channel identifier.</value> diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index d88943662..d1a51c2cf 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/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); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 344e996ea..498df5ab0 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/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)) { diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 81f2f02bc..df6829946 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/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; } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 2a86fded2..27154c297 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -780,6 +780,12 @@ namespace MediaBrowser.Model.Dto 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> /// <value>The current program.</value> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index b8578c46f..e1dcbc993 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/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); diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 280021955..114a92975 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/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( |
