aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props4
-rw-r--r--Emby.Dlna/PlayTo/DlnaHttpClient.cs39
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs12
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs1
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs20
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs25
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs6
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs10
-rw-r--r--Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs6
-rw-r--r--Jellyfin.Server/Startup.cs8
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs7
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs7
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs60
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs2
21 files changed, 163 insertions, 68 deletions
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 ff5f0efdf..fa6bead8b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -45,7 +45,7 @@
<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" />
@@ -56,7 +56,7 @@
<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="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
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, "&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();
}
}
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/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/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index adb8ac732..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
@@ -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/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 20995bf1b..8d2a738d4 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -65,14 +65,12 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="mediaType">The media type.</param>
/// <param name="createPlaylistRequest">The create playlist payload.</param>
/// <response code="200">Playlist created.</response>
- /// <response code="403">User does not have permission to create playlists.</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.
/// </returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
@@ -105,11 +103,9 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="ids">Item id, comma delimited.</param>
/// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response>
- /// <response code="403">User does not have permission to add items to playlist.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
@@ -127,11 +123,9 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="newIndex">The new index.</param>
/// <response code="204">Item moved to new index.</response>
- /// <response code="403">User does not have permission to move item.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> MoveItem(
[FromRoute, Required] string playlistId,
[FromRoute, Required] string itemId,
@@ -147,11 +141,9 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="playlistId">The playlist id.</param>
/// <param name="entryIds">The item ids, comma delimited.</param>
/// <response code="204">Items removed.</response>
- /// <response code="403">User does not have permission to get playlist.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> RemoveFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
@@ -173,12 +165,10 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Original playlist returned.</response>
- /// <response code="403">User does not have permission to get playlist items.</response>
/// <response code="404">Playlist not found.</response>
/// <returns>The original playlist items.</returns>
[HttpGet("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
[FromRoute, Required] Guid playlistId,
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 1dd938b1b..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;
@@ -60,13 +61,14 @@ internal class FixPlaylistOwner : IMigrationRoutine
{
playlist.OwnerUserId = guid;
playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
- _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult();
+ playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+ _playlistManager.SavePlaylistFile(playlist);
}
}
else
{
playlist.OpenAccess = true;
- _playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult();
+ playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 57efd5820..6394800f7 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.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(