Merge branch 'master' into network-rewrite

pull/8147/head
Shadowghost 2 years ago
commit 520c07e8ca

@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2
uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2

@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0

@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}

@ -17,30 +17,30 @@
<PackageVersion Include="Diacritics" Version="3.3.14" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.5" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.10" />
<PackageVersion Include="LrcParser" Version="2022.529.1" />
<PackageVersion Include="libse" Version="3.6.11" />
<PackageVersion Include="LrcParser" Version="2023.308.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
@ -52,7 +52,7 @@
<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.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />

@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app

@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu

@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
&& npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder)
IMediaEncoder mediaEncoder,
Device device)
{
_session = session;
_sessionManager = sessionManager;
@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
}
public bool IsSessionActive => !_disposed && _device is not null;
public bool SupportsMediaControl => IsSessionActive;
public void Init(Device device)
{
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart;
@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
public bool IsSessionActive => !_disposed;
public bool SupportsMediaControl => IsSessionActive;
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
}
}
private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
var info = e.Argument;
if (!_disposed
&& info.Headers.TryGetValue("USN", out string usn)
&& info.Headers.TryGetValue("USN", out string? usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|| (info.Headers.TryGetValue("NT", out string nt)
|| (info.Headers.TryGetValue("NT", out string? nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{
OnDeviceUnavailable();
}
}
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
{
if (_disposed)
{
@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
{
if (_disposed)
{
@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
}
}
private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (_disposed)
{
@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem(
BaseItem item,
User user,
User? user,
long startPostionTicks,
string mediaSourceId,
string? mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem;
}
private string GetDlnaHeaders(PlaylistItem item)
private string? GetDlnaHeaders(PlaylistItem item)
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
_device = null;
_disposed = true;
}
@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
if (command.Arguments.TryGetValue("Index", out string? index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
if (command.Arguments.TryGetValue("Volume", out string? vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name);
}
if (_device is null)
{
return Task.CompletedTask;
}
if (name == SessionMessageType.Play)
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (name == SessionMessageType.Playstate)
return name switch
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (name == SessionMessageType.GeneralCommand)
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
_ => Task.CompletedTask // Not supported or needed right now
};
}
private class StreamParams
{
private MediaSourceInfo _mediaSource;
private IMediaSourceManager _mediaSourceManager;
private MediaSourceInfo? _mediaSource;
private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; }
@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; }
public string DeviceProfileId { get; set; }
public string? DeviceProfileId { get; set; }
public string DeviceId { get; set; }
public string? DeviceId { get; set; }
public string MediaSourceId { get; set; }
public string? MediaSourceId { get; set; }
public string LiveStreamId { get; set; }
public string? LiveStreamId { get; set; }
public BaseItem Item { get; set; }
public BaseItem? Item { get; set; }
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
{
if (_mediaSource is not null)
{
@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{
var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
if (Guid.TryParse(parts[i + 1], out var result))
{

@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
_mediaEncoder);
_mediaEncoder,
device);
sessionInfo.AddController(controller);
controller.Init(device);
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();

@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
@ -529,6 +531,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();

@ -157,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
var channels = GetAllChannels()
.Select(GetChannelEntity)
var channels = await GetAllChannelEntitiesAsync()
.OrderBy(i => i.SortName)
.ToList();
.ToListAsync()
.ConfigureAwait(false);
if (query.IsRecordingsFolder.HasValue)
{
@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
if (user is not null)
{
var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
channels = channels.Where(i =>
{
if (!i.IsVisible(user))
@ -235,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
try
{
return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
return GetChannelProvider(i).IsEnabledFor(userId);
}
catch
{
@ -258,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
}
}
@ -269,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
var user = query.UserId.Equals(default)
? null
: _userManager.GetUserById(query.UserId);
var internalResult = GetChannelsInternal(query);
var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@ -327,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
progress.Report(100);
}
private Channel GetChannelEntity(IChannel channel)
private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
{
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
foreach (IChannel channel in GetAllChannels())
{
yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
}
}
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)

@ -276,25 +276,31 @@ namespace Emby.Server.Implementations.EntryPoints
/// Libraries the update timer callback.
/// </summary>
/// <param name="state">The state.</param>
private void LibraryUpdateTimerCallback(object state)
private async void LibraryUpdateTimerCallback(object state)
{
List<Folder> foldersAddedTo;
List<Folder> foldersRemovedFrom;
List<BaseItem> itemsUpdated;
List<BaseItem> itemsAdded;
List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
var foldersAddedTo = _foldersAddedTo
foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
var foldersRemovedFrom = _foldersRemovedFrom
foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
var itemsUpdated = _itemsUpdated
itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.ToList();
SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
itemsAdded = _itemsAdded.ToList();
itemsRemoved = _itemsRemoved.ToList();
if (LibraryUpdateTimer is not null)
{
@ -308,6 +314,8 @@ namespace Emby.Server.Implementations.EntryPoints
_foldersAddedTo.Clear();
_foldersRemovedFrom.Clear();
}
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>

@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
private void UpdateTimerCallback(object? state)
private async void UpdateTimerCallback(object? state)
{
List<KeyValuePair<Guid, List<BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
var changes = _changedItems.ToList();
changes = _changedItems.ToList();
_changedItems.Clear();
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
if (_updateTimer is not null)
{
_updateTimer.Dispose();
_updateTimer = null;
}
}
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
}
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
{
foreach (var pair in changes)
foreach ((var key, var value) in changes)
{
await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
}
}

@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
IMemoryCache memoryCache,
NamingOptions namingOptions)
NamingOptions namingOptions,
IDirectoryService directoryService)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
_memoryCache = memoryCache;
_namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true);
}
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
{
Parent = parent,
FileInfo = fileInfo,

@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
if (resolvedItem.Files.Count == 0)
// Until multi-part books are handled letting files stack hides them from browsing in the client
if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
{
continue;
}

@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
private readonly ILogger<MusicAlbumResolver> _logger;
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
/// <param name="directoryService">The directory service.</param>
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
_directoryService = directoryService;
}
/// <summary>
@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If args contains music it's a music album
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
{
return true;
}

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
private readonly ILogger<MusicAlbumResolver> _logger;
private NamingOptions _namingOptions;
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
/// <param name="directoryService">The directory service.</param>
public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger,
NamingOptions namingOptions)
NamingOptions namingOptions,
IDirectoryService directoryService)
{
_logger = logger;
_namingOptions = namingOptions;
_directoryService = directoryService;
}
/// <summary>
@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
var directoryService = args.DirectoryService;
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// If we contain a music album assume we are an artist folder
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{
// Stop once we see a music album
state.Stop();

@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
private readonly ILogger _logger;
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_logger = logger;
NamingOptions = namingOptions;
DirectoryService = directoryService;
}
protected NamingOptions NamingOptions { get; }
protected IDirectoryService DirectoryService { get; }
/// <summary>
/// Resolves the specified args.
/// </summary>
@ -65,13 +68,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = child.Name;
if (child.IsDirectory)
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
if (IsDvdDirectory(child.FullName, filename, DirectoryService))
{
videoType = VideoType.Dvd;
var videoTmp = new TVideoType
{
Path = args.Path,
VideoType = VideoType.Dvd
};
Set3DFormat(videoTmp);
return videoTmp;
}
else if (IsBluRayDirectory(filename))
{
videoType = VideoType.BluRay;
var videoTmp = new TVideoType
{
Path = args.Path,
VideoType = VideoType.BluRay
};
Set3DFormat(videoTmp);
return videoTmp;
}
}
else if (IsDvdFile(filename))

@ -4,6 +4,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@ -25,11 +26,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
/// <param name="directoryService">The directory service.</param>
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
{
_namingOptions = namingOptions;
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) };
}
/// <summary>

@ -2,6 +2,7 @@
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers
@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
: base(logger, namingOptions)
/// <param name="directoryService">The directory service.</param>
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{
}
}

@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
: base(logger, namingOptions)
/// <param name="directoryService">The directory service.</param>
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
}
@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
// ignore extras

@ -1,7 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
@ -12,15 +10,20 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
/// <summary>
/// Class PhotoResolver.
/// </summary>
public class PhotoResolver : ItemResolver<Photo>
{
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default"
};
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
/// <summary>
/// Initializes a new instance of the <see cref="PhotoResolver"/> class.
/// </summary>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
{
_imageProcessor = imageProcessor;
_namingOptions = namingOptions;
_directoryService = directoryService;
}
/// <summary>
@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var filename = Path.GetFileNameWithoutExtension(args.Path);
// Make sure the image doesn't belong to a video file
var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
foreach (var file in files)
{

@ -5,6 +5,7 @@ using System.Linq;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
: base(logger, namingOptions)
/// <param name="directoryService">The directory service.</param>
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{
}

@ -111,10 +111,10 @@ namespace Emby.Server.Implementations.Library
if (query.IncludeExternalContent)
{
var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
UserId = query.UserId
});
}).GetAwaiter().GetResult();
var channels = channelResult.Items;

@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
return 7;
}
private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user is null)
{
return new QueryResult<BaseItem>();
}
var folderIds = GetRecordingFolders(user, true)
.Select(i => i.Id)
.ToList();
var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
var folderIds = Array.ConvertAll(folders, x => x.Id);
var excludeItemTypes = new List<BaseItemKind>();
if (folderIds.Count == 0)
if (folderIds.Length == 0)
{
return new QueryResult<BaseItem>();
}
@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
{
MediaTypes = new[] { MediaType.Video },
Recursive = true,
AncestorIds = folderIds.ToArray(),
AncestorIds = folderIds,
IsFolder = false,
IsVirtualItem = false,
Limit = limit,
@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
{
var user = query.UserId.Equals(default)
? null
@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
RemoveFields(options);
var internalResult = GetEmbyRecordings(query, options, user);
var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
return _tvDtoService.GetInternalProgramId(externalId);
}
public List<BaseItem> GetRecordingFolders(User user)
{
return GetRecordingFolders(user, false);
}
/// <inheritdoc />
public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
=> GetRecordingFoldersAsync(user, false);
private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
{
var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
.SelectMany(i => i.Locations)
@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName)
.ToList();
folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
}).Items);
}).ConfigureAwait(false);
folders.AddRange(channels.Items);
return folders.Cast<BaseItem>().ToList();
return folders.Cast<BaseItem>().ToArray();
}
}
}

@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var attributes = ParseExtInf(extInf, out string remaining);
extInf = remaining;
if (attributes.TryGetValue("tvg-logo", out string value))
if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
{
channel.ImageUrl = value;
channel.ImageUrl = tvgLogo;
}
else if (attributes.TryGetValue("logo", out string logo))
{
channel.ImageUrl = logo;
}
if (attributes.TryGetValue("group-title", out string groupTitle))

@ -107,5 +107,14 @@
"TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn",
"TasksMaintenanceCategory": "Viðhald",
"Default": "Sjálfgefið"
"Default": "Sjálfgefið",
"TaskCleanActivityLog": "Hreinsa athafnaskrá",
"TaskRefreshPeople": "Endurnýja fólk",
"TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
"TaskOptimizeDatabase": "Fínstilla gagnagrunn",
"Undefined": "Óskilgreint",
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
"HearingImpaired": "Heyrnarskertur"
}

@ -184,10 +184,19 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
{
var ratings = GetParentalRatingsDictionary().Values.ToList();
// Use server default language for ratings
// Fall back to empty list if there are no parental ratings for that language
var ratings = GetParentalRatingsDictionary()?.Values.ToList()
?? new List<ParentalRating>();
// Add common ratings to ensure them being available for selection.
// Add common ratings to ensure them being available for selection
// Based on the US rating system due to it being the main source of rating in the metadata providers
// Unrated
if (!ratings.Any(x => x.Value is null))
{
ratings.Add(new ParentalRating("Unrated", null));
}
// Minimum rating possible
if (!ratings.Any(x => x.Value == 0))
{
@ -237,36 +246,26 @@ namespace Emby.Server.Implementations.Localization
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
/// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
{
var countryCode = _configurationManager.Configuration.MetadataCountryCode;
// Fall back to US ratings if no country code is specified or country code does not exist.
// Fallback to server default if no country code is specified.
if (string.IsNullOrEmpty(countryCode))
{
countryCode = "us";
countryCode = _configurationManager.Configuration.MetadataCountryCode;
}
return GetRatings(countryCode)
?? GetRatings("us")
?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
}
/// <summary>
/// Gets the ratings for a country.
/// </summary>
/// <param name="countryCode">The country code.</param>
/// <returns>The ratings.</returns>
private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
{
_allParentalRatings.TryGetValue(countryCode, out var countryValue);
if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
{
return countryValue;
}
return countryValue;
return null;
}
/// <inheritdoc />
public int? GetRatingLevel(string rating)
public int? GetRatingLevel(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
@ -280,32 +279,51 @@ namespace Emby.Server.Implementations.Localization
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
var ratingsDictionary = GetParentalRatingsDictionary();
if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
// Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode))
{
var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
{
return value.Value;
}
}
else
{
return value.Value;
// Fall back to server default language for ratings check
// If it has no ratings, use the US ratings
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
{
return value.Value;
}
}
// If we don't find anything check all ratings systems
// If we don't find anything, check all ratings systems
foreach (var dictionary in _allParentalRatings.Values)
{
if (dictionary.TryGetValue(rating, out value))
if (dictionary.TryGetValue(rating, out var value))
{
return value.Value;
}
}
// Try splitting by : to handle "Germany: FSK 18"
// Try splitting by : to handle "Germany: FSK-18"
if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart(':').ToString());
}
// Remove prefix country code to handle "DE-18"
// Handle prefix country code to handle "DE-18"
if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
{
return GetRatingLevel(rating.AsSpan().RightPart('-').ToString());
var ratingSpan = rating.AsSpan();
// Extract culture from country prefix
var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
// Check rating system of culture
return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName);
}
return null;

@ -52,7 +52,7 @@ public class ChannelsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels(
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@ -61,7 +61,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] bool? isFavorite)
{
userId = RequestHelpers.GetUserId(User, userId);
return _channelManager.GetChannels(new ChannelQuery
return await _channelManager.GetChannelsAsync(new ChannelQuery
{
Limit = limit,
StartIndex = startIndex,
@ -69,7 +69,7 @@ public class ChannelsController : BaseJellyfinApiController
SupportsLatestItems = supportsLatestItems,
SupportsMediaDeletion = supportsMediaDeletion,
IsFavorite = isFavorite
});
}).ConfigureAwait(false);
}
/// <summary>

@ -19,6 +19,8 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@ -1654,8 +1656,8 @@ public class DynamicHlsController : BaseJellyfinApiController
startNumber.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
isEventPlaylist ? "event" : "vod",
outputTsArg,
outputPath).Trim();
EncodingUtils.NormalizePath(outputTsArg),
EncodingUtils.NormalizePath(outputPath)).Trim();
}
/// <summary>
@ -1840,7 +1842,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
// video processing filters.
args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam);
args = negativeMapArgs + args + videoProcessParam;
// -start_at_zero is necessary to use with -ss when seeking,
// otherwise the target position cannot be determined.

@ -246,6 +246,11 @@ public class ItemUpdateController : BaseJellyfinApiController
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
}
if (request.Height is not null && item is LiveTvChannel channel)
{
channel.Height = request.Height.Value;
}
item.Tags = request.Tags;
if (request.Taglines is not null)

@ -252,7 +252,7 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings(
[FromQuery] string? channelId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
@ -278,7 +278,7 @@ public class LiveTvController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return _liveTvManager.GetRecordings(
return await _liveTvManager.GetRecordingsAsync(
new RecordingQuery
{
ChannelId = channelId,
@ -299,7 +299,7 @@ public class LiveTvController : BaseJellyfinApiController
ImageTypeLimit = imageTypeLimit,
EnableImages = enableImages
},
dtoOptions);
dtoOptions).ConfigureAwait(false);
}
/// <summary>
@ -383,13 +383,13 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Recordings/Folders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var folders = _liveTvManager.GetRecordingFolders(user);
var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);

@ -323,6 +323,15 @@ public class TranscodingJobHelper : IDisposable
if (delete(job.Path!))
{
await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
if (job.MediaSource?.VideoType == VideoType.Dvd || job.MediaSource?.VideoType == VideoType.BluRay)
{
var concatFilePath = Path.Join(_serverConfigurationManager.GetTranscodePath(), job.MediaSource.Id + ".concat");
if (File.Exists(concatFilePath))
{
_logger.LogInformation("Deleting ffmpeg concat configuration at {Path}", concatFilePath);
File.Delete(concatFilePath);
}
}
}
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
@ -524,7 +533,10 @@ public class TranscodingJobHelper : IDisposable
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
if (state.VideoType != VideoType.Dvd)
{
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
{

@ -22,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>

@ -56,8 +56,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
base.Dispose(dispose);
}
private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
SendData(true).GetAwaiter().GetResult();
await SendData(true).ConfigureAwait(false);
}
}

@ -46,14 +46,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
/// <summary>
/// Gets the latest channel items.

@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities
var path = ContainingFolderPath;
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path)
};

@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
= new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
= new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{

@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities
{
var path = ContainingFolderPath;
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
Parent = GetParent() as Folder,

@ -1,12 +1,11 @@
#nullable disable
#pragma warning disable CA1721, CA1819, CS1591
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@ -23,22 +22,20 @@ namespace MediaBrowser.Controller.Library
/// </summary>
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
/// </summary>
/// <param name="appPaths">The app paths.</param>
/// <param name="directoryService">The directory service.</param>
public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService)
/// <param name="libraryManager">The library manager.</param>
public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager)
{
_appPaths = appPaths;
DirectoryService = directoryService;
_libraryManager = libraryManager;
}
// TODO remove dependencies as properties, they should be injected where it makes sense
public IDirectoryService DirectoryService { get; }
/// <summary>
/// Gets or sets the file system children.
/// </summary>
@ -47,7 +44,7 @@ namespace MediaBrowser.Controller.Library
public LibraryOptions LibraryOptions
{
get => _libraryOptions ??= Parent is null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent);
set => _libraryOptions = value;
}
@ -231,21 +228,15 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the configured content type for the path.
/// </summary>
/// <remarks>
/// This is subject to future refactoring as it relies on a static property in BaseItem.
/// </remarks>
/// <returns>The configured content type.</returns>
public string GetConfiguredContentType()
{
return BaseItem.LibraryManager.GetConfiguredContentType(Path);
return _libraryManager.GetConfiguredContentType(Path);
}
/// <summary>
/// Gets the file system children that do not hit the ignore file check.
/// </summary>
/// <remarks>
/// This is subject to future refactoring as it relies on a static property in BaseItem.
/// </remarks>
/// <returns>The file system children that are not ignored.</returns>
public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren()
{
@ -253,7 +244,7 @@ namespace MediaBrowser.Controller.Library
for (var i = 0; i < numberOfChildren; i++)
{
var child = FileSystemChildren[i];
if (BaseItem.LibraryManager.IgnoreFile(child, Parent))
if (_libraryManager.IgnoreFile(child, Parent))
{
continue;
}

@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <returns>A recording.</returns>
QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options);
/// <summary>
/// Gets the timers.
@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
List<BaseItem> GetRecordingFolders(User user);
Task<BaseItem[]> GetRecordingFoldersAsync(User user);
}
}

@ -43,6 +43,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
"ConstrainedBaseline",
@ -558,9 +561,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetInputPathArgument(EncodingJobInfo state)
{
var mediaPath = state.MediaPath ?? string.Empty;
return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource);
return state.MediaSource.VideoType switch
{
VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
_ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
};
}
/// <summary>
@ -639,6 +645,26 @@ namespace MediaBrowser.Controller.MediaEncoding
deviceIndex);
}
private string GetVulkanDeviceArgs(int deviceIndex, string deviceName, string srcDeviceAlias, string alias)
{
alias ??= VulkanAlias;
deviceIndex = deviceIndex >= 0
? deviceIndex
: 0;
var vendorOpts = string.IsNullOrEmpty(deviceName)
? ":" + deviceIndex
: ":" + "\"" + deviceName + "\"";
var options = string.IsNullOrEmpty(srcDeviceAlias)
? vendorOpts
: "@" + srcDeviceAlias;
return string.Format(
CultureInfo.InvariantCulture,
" -init_hw_device vulkan={0}{1}",
alias,
options);
}
private string GetOpenclDeviceArgs(int deviceIndex, string deviceVendorName, string srcDeviceAlias, string alias)
{
alias ??= OpenclAlias;
@ -821,6 +847,12 @@ namespace MediaBrowser.Controller.MediaEncoding
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
else
{
// libplacebo wants an explicitly set vulkan filter device.
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
}
}
else
{
@ -962,8 +994,18 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(canvasArgs);
}
arg.Append(" -i ")
.Append(GetInputPathArgument(state));
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
arg.Append(" -f concat -safe 0 -i ")
.Append(tmpConcatPath);
}
else
{
arg.Append(" -i ")
.Append(GetInputPathArgument(state));
}
// sub2video for external graphical subtitles
if (state.SubtitleStream is not null
@ -2074,14 +2116,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private static double GetVideoBitrateScaleFactor(string codec)
{
// hevc & vp9 - 40% more efficient than h.264
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .6;
}
// av1 - 50% more efficient than h.264
if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
{
return .5;
}
return 1;
}
@ -2089,7 +2137,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
var scaleFactor = outputScaleFactor / inputScaleFactor;
// Don't scale the real bitrate lower than the requested bitrate
var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@ -2429,6 +2479,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return args;
}
/// <summary>
/// Gets the negative map args by filters.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="videoProcessFilters">The videoProcessFilters.</param>
/// <returns>System.String.</returns>
public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters)
{
string args = string.Empty;
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
{
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
args += string.Format(
CultureInfo.InvariantCulture,
"-map -0:{0} ",
videoStreamIndex);
}
return args;
}
/// <summary>
/// Determines which stream will be used for playback.
/// </summary>
@ -3271,7 +3345,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap";
var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read";
mainFilters.Add(hwTransferFilter);
mainFilters.Add("format=nv12");
}
@ -3514,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
// qsv hwmap is not fully implemented for the time being.
mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@ -3672,6 +3746,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
{
hwScaleFilter += ":extra_hw_frames=24";
}
// hw scale
mainFilters.Add(hwScaleFilter);
}
@ -3718,7 +3799,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
// qsv hwmap is not fully implemented for the time being.
mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@ -3947,6 +4028,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
{
hwScaleFilter += ":extra_hw_frames=24";
}
// hw scale
mainFilters.Add(hwScaleFilter);
}
@ -3988,7 +4076,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap");
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
mainFilters.Add("format=nv12");
}
@ -4126,7 +4214,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw
if (doVkTonemap)
{
mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16");
mainFilters.Add("hwupload_vaapi");
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan");
}
}
else if (isVaapiDecoder)
@ -4156,6 +4246,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan");
}
// vk tonemap
@ -4234,7 +4325,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefer vaapi hwupload to vulkan hwupload,
// Mesa RADV does not support a dedicated transfer queue.
subFilters.Add("hwupload=derive_device=vaapi,format=vaapi,hwmap=derive_device=vulkan");
subFilters.Add("hwupload_vaapi");
subFilters.Add("hwmap=derive_device=vulkan");
subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("scale_vulkan=format=nv12");
@ -4336,6 +4429,13 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
// allocate extra pool sizes for vaapi vpp
if (!string.IsNullOrEmpty(hwScaleFilter))
{
hwScaleFilter += ":extra_hw_frames=24";
}
// hw scale
mainFilters.Add(hwScaleFilter);
}
@ -4713,7 +4813,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// HWA decoders can handle both video files and video folders.
var videoType = mediaSource.VideoType;
var videoType = state.VideoType;
if (videoType != VideoType.VideoFile
&& videoType != VideoType.Iso
&& videoType != VideoType.Dvd
@ -4854,8 +4954,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
var ffmpegVersion = _mediaEncoder.EncoderVersion;
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
&& string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
// Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase);
// Disable the extra internal copy in nvdec. We already handle it in filter chain.
var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
if (bitDepth == 10 && isCodecAvailable)
{
@ -4881,14 +4991,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiSupported && isCodecAvailable)
{
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
if (isD3d11Supported && isCodecAvailable)
{
// set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
else
@ -4908,7 +5020,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (options.EnableEnhancedNvdecDecoder)
{
// set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
else
{
@ -4923,7 +5036,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isD3d11Supported && isCodecAvailable)
{
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@ -4932,9 +5046,11 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVaapiSupported
&& isCodecAvailable)
{
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
// Apple videotoolbox
if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
&& isVideotoolboxSupported
&& isCodecAvailable)
@ -5738,7 +5854,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// video processing filters.
var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec);
args += videoProcessParam;
var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam);
args = negativeMapArgs + args + videoProcessParam;
hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase);

@ -153,6 +153,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
/// <summary>
/// Gets the input argument.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns>
string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource);
/// <summary>
/// Gets the input argument for an external subtitle file.
/// </summary>
@ -187,5 +195,27 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="path">The path.</param>
/// <param name="pathType">The type of path.</param>
void UpdateEncoderPath(string path, string pathType);
/// <summary>
/// Gets the primary playlist of .vob files.
/// </summary>
/// <param name="path">The to the .vob files.</param>
/// <param name="titleNumber">The title number to start with.</param>
/// <returns>A playlist.</returns>
IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
/// <summary>
/// Gets the primary playlist of .m2ts files.
/// </summary>
/// <param name="path">The to the .m2ts files.</param>
/// <returns>A playlist.</returns>
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
/// <summary>
/// Generates a FFmpeg concat config for the source.
/// </summary>
/// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
/// <param name="concatFilePath">The path the config should be written to.</param>
void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath);
}
}

@ -14,6 +14,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -301,10 +302,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
inputPath,
attachmentStreamIndex,
outputPath);
EncodingUtils.NormalizePath(outputPath));
int exitCode;

@ -0,0 +1,123 @@
using System;
using System.IO;
using System.Linq;
using BDInfo.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo;
/// <summary>
/// Class BdInfoDirectoryInfo.
/// </summary>
public class BdInfoDirectoryInfo : IDirectoryInfo
{
private readonly IFileSystem _fileSystem;
private readonly FileSystemMetadata _impl;
/// <summary>
/// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class.
/// </summary>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="path">The path.</param>
public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
{
_fileSystem = fileSystem;
_impl = _fileSystem.GetDirectoryInfo(path);
}
private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
{
_fileSystem = fileSystem;
_impl = impl;
}
/// <summary>
/// Gets the name.
/// </summary>
public string Name => _impl.Name;
/// <summary>
/// Gets the full name.
/// </summary>
public string FullName => _impl.FullName;
/// <summary>
/// Gets the parent directory information.
/// </summary>
public IDirectoryInfo? Parent
{
get
{
var parentFolder = Path.GetDirectoryName(_impl.FullName);
if (parentFolder is not null)
{
return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
}
return null;
}
}
/// <summary>
/// Gets the directories.
/// </summary>
/// <returns>An array with all directories.</returns>
public IDirectoryInfo[] GetDirectories()
{
return _fileSystem.GetDirectories(_impl.FullName)
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
.ToArray();
}
/// <summary>
/// Gets the files.
/// </summary>
/// <returns>All files of the directory.</returns>
public IFileInfo[] GetFiles()
{
return _fileSystem.GetFiles(_impl.FullName)
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
/// <summary>
/// Gets the files matching a pattern.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
/// <returns>All files of the directory matchign the search pattern.</returns>
public IFileInfo[] GetFiles(string searchPattern)
{
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
/// <summary>
/// Gets the files matching a pattern and search options.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
/// <param name="searchOption">The search optin.</param>
/// <returns>All files of the directory matchign the search pattern and options.</returns>
public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
{
return _fileSystem.GetFiles(
_impl.FullName,
new[] { searchPattern },
false,
(searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories)
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
/// <summary>
/// Gets the bdinfo of a file system path.
/// </summary>
/// <param name="fs">The file system.</param>
/// <param name="path">The path.</param>
/// <returns>The BD directory information of the path on the file system.</returns>
public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
{
return new BdInfoDirectoryInfo(fs, path);
}
}

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BDInfo;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.BdInfo;
/// <summary>
/// Class BdInfoExaminer.
/// </summary>
public class BdInfoExaminer : IBlurayExaminer
{
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
/// </summary>
/// <param name="fileSystem">The filesystem.</param>
public BdInfoExaminer(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
/// <summary>
/// Gets the disc info.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BlurayDiscInfo.</returns>
public BlurayDiscInfo GetDiscInfo(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(nameof(path));
}
var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
bdrom.Scan();
// Get the longest playlist
var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
var outputStream = new BlurayDiscInfo
{
MediaStreams = Array.Empty<MediaStream>()
};
if (playlist is null)
{
return outputStream;
}
outputStream.Chapters = playlist.Chapters.ToArray();
outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
var sortedStreams = playlist.SortedStreams;
var mediaStreams = new List<MediaStream>(sortedStreams.Count);
foreach (var stream in sortedStreams)
{
switch (stream)
{
case TSVideoStream videoStream:
AddVideoStream(mediaStreams, videoStream);
break;
case TSAudioStream audioStream:
AddAudioStream(mediaStreams, audioStream);
break;
case TSTextStream textStream:
AddSubtitleStream(mediaStreams, textStream);
break;
case TSGraphicsStream graphicStream:
AddSubtitleStream(mediaStreams, graphicStream);
break;
}
}
outputStream.MediaStreams = mediaStreams.ToArray();
outputStream.PlaylistName = playlist.Name;
if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0)
{
// Get the files in the playlist
outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
}
return outputStream;
}
/// <summary>
/// Adds the video stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="videoStream">The video stream.</param>
private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
{
var mediaStream = new MediaStream
{
BitRate = Convert.ToInt32(videoStream.BitRate),
Width = videoStream.Width,
Height = videoStream.Height,
Codec = videoStream.CodecShortName,
IsInterlaced = videoStream.IsInterlaced,
Type = MediaStreamType.Video,
Index = streams.Count
};
if (videoStream.FrameRateDenominator > 0)
{
float frameRateEnumerator = videoStream.FrameRateEnumerator;
float frameRateDenominator = videoStream.FrameRateDenominator;
mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
}
streams.Add(mediaStream);
}
/// <summary>
/// Adds the audio stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="audioStream">The audio stream.</param>
private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
{
var stream = new MediaStream
{
Codec = audioStream.CodecShortName,
Language = audioStream.LanguageCode,
Channels = audioStream.ChannelCount,
SampleRate = audioStream.SampleRate,
Type = MediaStreamType.Audio,
Index = streams.Count
};
var bitrate = Convert.ToInt32(audioStream.BitRate);
if (bitrate > 0)
{
stream.BitRate = bitrate;
}
if (audioStream.LFE > 0)
{
stream.Channels = audioStream.ChannelCount + 1;
}
streams.Add(stream);
}
/// <summary>
/// Adds the subtitle stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="textStream">The text stream.</param>
private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
{
streams.Add(new MediaStream
{
Language = textStream.LanguageCode,
Codec = textStream.CodecShortName,
Type = MediaStreamType.Subtitle,
Index = streams.Count
});
}
/// <summary>
/// Adds the subtitle stream.
/// </summary>
/// <param name="streams">The streams.</param>
/// <param name="textStream">The text stream.</param>
private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
{
streams.Add(new MediaStream
{
Language = textStream.LanguageCode,
Codec = textStream.CodecShortName,
Type = MediaStreamType.Subtitle,
Index = streams.Count
});
}
}

@ -0,0 +1,68 @@
using System.IO;
using MediaBrowser.Model.IO;
namespace MediaBrowser.MediaEncoding.BdInfo;
/// <summary>
/// Class BdInfoFileInfo.
/// </summary>
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
private FileSystemMetadata _impl;
/// <summary>
/// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
/// </summary>
/// <param name="impl">The <see cref="FileSystemMetadata" />.</param>
public BdInfoFileInfo(FileSystemMetadata impl)
{
_impl = impl;
}
/// <summary>
/// Gets the name.
/// </summary>
public string Name => _impl.Name;
/// <summary>
/// Gets the full name.
/// </summary>
public string FullName => _impl.FullName;
/// <summary>
/// Gets the extension.
/// </summary>
public string Extension => _impl.Extension;
/// <summary>
/// Gets the length.
/// </summary>
public long Length => _impl.Length;
/// <summary>
/// Gets a value indicating whether this is a directory.
/// </summary>
public bool IsDir => _impl.IsDirectory;
/// <summary>
/// Gets a file as file stream.
/// </summary>
/// <returns>A <see cref="FileStream" /> for the file.</returns>
public Stream OpenRead()
{
return new FileStream(
FullName,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
}
/// <summary>
/// Gets a files's content with a stream reader.
/// </summary>
/// <returns>A <see cref="StreamReader" /> for the file's content.</returns>
public StreamReader OpenText()
{
return new StreamReader(OpenRead());
}
}

@ -1,7 +1,9 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Encoder
@ -15,21 +17,38 @@ namespace MediaBrowser.MediaEncoding.Encoder
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile);
}
return GetConcatInputArgument(inputFile, inputPrefix);
return GetFileInputArgument(inputFile, inputPrefix);
}
public static string GetInputArgument(string inputPrefix, IReadOnlyList<string> inputFiles, MediaProtocol protocol)
{
if (protocol != MediaProtocol.File)
{
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]);
}
return GetConcatInputArgument(inputFiles, inputPrefix);
}
/// <summary>
/// Gets the concat input argument.
/// </summary>
/// <param name="inputFile">The input file.</param>
/// <param name="inputFiles">The input files.</param>
/// <param name="inputPrefix">The input prefix.</param>
/// <returns>System.String.</returns>
private static string GetConcatInputArgument(string inputFile, string inputPrefix)
private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles, string inputPrefix)
{
// Get all streams
// If there's more than one we'll need to use the concat command
if (inputFiles.Count > 1)
{
var files = string.Join("|", inputFiles.Select(NormalizePath));
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
}
// Determine the input path for video files
return GetFileInputArgument(inputFile, inputPrefix);
return GetFileInputArgument(inputFiles[0], inputPrefix);
}
/// <summary>
@ -56,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
private static string NormalizePath(string path)
public static string NormalizePath(string path)
{
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
return path.Replace("\"", "\\\"", StringComparison.Ordinal);

@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@ -51,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
private readonly IBlurayExaminer _blurayExaminer;
private readonly IConfiguration _config;
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
@ -95,6 +97,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
ILogger<MediaEncoder> logger,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IConfiguration config,
IServerConfigurationManager serverConfig)
@ -102,6 +105,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_blurayExaminer = blurayExaminer;
_localization = localization;
_config = config;
_serverConfig = serverConfig;
@ -117,16 +121,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string ProbePath => _ffprobePath;
/// <inheritdoc />
public Version EncoderVersion => _ffmpegVersion;
/// <inheritdoc />
public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
/// <inheritdoc />
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
/// <inheritdoc />
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
/// <inheritdoc />
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
/// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
/// <summary>
@ -344,26 +354,31 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffmpegVersion = validator.GetFFmpegVersion();
}
/// <inheritdoc />
public bool SupportsEncoder(string encoder)
{
return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public bool SupportsDecoder(string decoder)
{
return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public bool SupportsHwaccel(string hwaccel)
{
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public bool SupportsFilter(string filter)
{
return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public bool SupportsFilterWithOption(FilterOptionType option)
{
if (_filtersWithOption.TryGetValue((int)option, out var val))
@ -394,24 +409,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true;
}
/// <summary>
/// Gets the media info.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <inheritdoc />
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
var inputFile = request.MediaSource.Path;
string analyzeDuration = string.Empty;
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0)
{
analyzeDuration = "-analyzeduration " +
(request.MediaSource.AnalyzeDurationMs * 1000).ToString();
analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
}
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
@ -419,7 +426,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
return GetMediaInfoInternal(
GetInputArgument(inputFile, request.MediaSource),
GetInputArgument(request.MediaSource.Path, request.MediaSource),
request.MediaSource.Path,
request.MediaSource.Protocol,
extractChapters,
@ -429,36 +436,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
cancellationToken);
}
/// <summary>
/// Gets the input argument.
/// </summary>
/// <param name="inputFile">The input file.</param>
/// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentException">Unrecognized InputType.</exception>
/// <inheritdoc />
public string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource)
{
return EncodingUtils.GetInputArgument("file", inputFiles, mediaSource.Protocol);
}
/// <inheritdoc />
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
if (mediaSource.VideoType == VideoType.BluRay
|| mediaSource.IsoType == IsoType.BluRay)
if (mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol);
return EncodingUtils.GetInputArgument(prefix, new[] { inputFile }, mediaSource.Protocol);
}
/// <summary>
/// Gets the input argument for an external subtitle file.
/// </summary>
/// <param name="inputFile">The input file.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentException">Unrecognized InputType.</exception>
/// <inheritdoc />
public string GetExternalSubtitleInputArgument(string inputFile)
{
const string Prefix = "file";
return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File);
return EncodingUtils.GetInputArgument(Prefix, new[] { inputFile }, MediaProtocol.File);
}
/// <summary>
@ -549,6 +550,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
/// <inheritdoc />
public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken)
{
var mediaSource = new MediaSourceInfo
@ -559,11 +561,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken);
}
/// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken);
}
/// <inheritdoc />
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
@ -767,6 +771,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
/// <inheritdoc />
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@ -865,6 +870,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new NotImplementedException();
}
/// <inheritdoc />
public IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
{
// Eliminate menus and intros by omitting VIDEO_TS.VOB and all subsequent title .vob files ending with _0.VOB
var allVobs = _fileSystem.GetFiles(path, true)
.Where(file => string.Equals(file.Extension, ".VOB", StringComparison.OrdinalIgnoreCase))
.Where(file => !string.Equals(file.Name, "VIDEO_TS.VOB", StringComparison.OrdinalIgnoreCase))
.Where(file => !file.Name.EndsWith("_0.VOB", StringComparison.OrdinalIgnoreCase))
.OrderBy(i => i.FullName)
.ToList();
if (titleNumber.HasValue)
{
var prefix = string.Format(CultureInfo.InvariantCulture, "VTS_{0:D2}_", titleNumber.Value);
var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
if (vobs.Count > 0)
{
return vobs.Select(i => i.FullName).ToList();
}
_logger.LogWarning("Could not determine .vob files for title {Title} of {Path}.", titleNumber, path);
}
// Check for multiple big titles (> 900 MB)
var titles = allVobs
.Where(vob => vob.Length >= 900 * 1024 * 1024)
.Select(vob => _fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString())
.Distinct()
.ToList();
// Fall back to first title if no big title is found
if (titles.Count == 0)
{
titles.Add(_fileSystem.GetFileNameWithoutExtension(allVobs[0]).AsSpan().RightPart('_').ToString());
}
// Aggregate all .vob files of the titles
return allVobs
.Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()))
.Select(i => i.FullName)
.ToList();
}
/// <inheritdoc />
public IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path)
{
// Get all playable .m2ts files
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
// Get all files from the BDMV/STREAMING directory
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
// Only return playable local .m2ts files
return directoryFiles
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
.Select(f => f.FullName)
.ToList();
}
/// <inheritdoc />
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
{
// Get all playable files
IReadOnlyList<string> files;
var videoType = source.VideoType;
if (videoType == VideoType.Dvd)
{
files = GetPrimaryPlaylistVobFiles(source.Path, null);
}
else if (videoType == VideoType.BluRay)
{
files = GetPrimaryPlaylistM2tsFiles(source.Path);
}
else
{
return;
}
// Generate concat configuration entries for each file and write to file
using (StreamWriter sw = new StreamWriter(concatFilePath))
{
foreach (var path in files)
{
var mediaInfoResult = GetMediaInfo(
new MediaInfoRequest
{
MediaType = DlnaProfileType.Video,
MediaSource = new MediaSourceInfo
{
Path = path,
Protocol = MediaProtocol.File,
VideoType = videoType
}
},
CancellationToken.None).GetAwaiter().GetResult();
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
// Add file path stanza to concat configuration
sw.WriteLine("file '{0}'", path);
// Add duration stanza to concat configuration
sw.WriteLine("duration {0}", duration);
}
}
}
public bool CanExtractSubtitles(string codec)
{
// TODO is there ever a case when a subtitle can't be extracted??

@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BDInfo" />
<PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Text.Encoding.CodePages" />

@ -248,12 +248,23 @@ namespace MediaBrowser.MediaEncoding.Probing
return null;
}
// Handle MPEG-1 container
if (string.Equals(format, "mpegvideo", StringComparison.OrdinalIgnoreCase))
{
return "mpeg";
}
format = format.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
// Handle MPEG-2 container
if (string.Equals(format, "mpeg", StringComparison.OrdinalIgnoreCase))
{
return "ts";
}
// Handle matroska container
if (string.Equals(format, "matroska", StringComparison.OrdinalIgnoreCase))
{
return "mkv";
}
return format;
}

@ -39,7 +39,8 @@ public class EncodingOptions
DeinterlaceMethod = "yadif";
EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true;
EnableEnhancedNvdecDecoder = false;
// Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping.
EnableEnhancedNvdecDecoder = true;
PreferSystemNativeHwDecoder = true;
EnableIntelLowPowerH264HwEncoder = false;
EnableIntelLowPowerHevcHwEncoder = false;

@ -243,16 +243,10 @@ namespace MediaBrowser.Model.Configuration
public bool AllowClientLogUpload { get; set; } = true;
/// <summary>
/// Gets or sets the dummy chapters duration in seconds.
/// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
/// </summary>
/// <value>The dummy chapters duration.</value>
public int DummyChapterDuration { get; set; } = 300;
/// <summary>
/// Gets or sets the dummy chapter count.
/// </summary>
/// <value>The dummy chapter count.</value>
public int DummyChapterCount { get; set; } = 100;
public int DummyChapterDuration { get; set; } = 0;
/// <summary>
/// Gets or sets the chapter image resolution.

@ -136,12 +136,26 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
var conditionType = condition.Condition;
if (condition.Condition == ProfileConditionType.EqualsAny)
{
switch (condition.Condition)
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
{
if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue)
&& conditionValue.Equals(currentValue))
{
return true;
}
}
return false;
}
if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected))
{
switch (conditionType)
{
case ProfileConditionType.Equals:
case ProfileConditionType.EqualsAny:
return currentValue.Value.Equals(expected);
case ProfileConditionType.GreaterThanEqual:
return currentValue.Value >= expected;
@ -212,9 +226,24 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
var conditionType = condition.Condition;
if (condition.Condition == ProfileConditionType.EqualsAny)
{
switch (condition.Condition)
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
{
if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue)
&& conditionValue.Equals(currentValue))
{
return true;
}
}
return false;
}
if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected))
{
switch (conditionType)
{
case ProfileConditionType.Equals:
return currentValue.Value.Equals(expected);

@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>();
public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>();
[XmlAttribute("container")]
public string Container { get; set; } = string.Empty;

@ -18,17 +18,17 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
public bool SupportsContainer(string container)
public bool SupportsContainer(string? container)
{
return ContainerProfile.ContainsContainer(Container, container);
}
public bool SupportsVideoCodec(string codec)
public bool SupportsVideoCodec(string? codec)
{
return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec);
}
public bool SupportsAudioCodec(string codec)
public bool SupportsAudioCodec(string? codec)
{
return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec);
}

@ -10,22 +10,4 @@ namespace MediaBrowser.Model.Dlna
bool CanExtractSubtitles(string codec);
}
public class FullTranscoderSupport : ITranscoderSupport
{
public bool CanEncodeToAudioCodec(string codec)
{
return true;
}
public bool CanEncodeToSubtitleCodec(string codec)
{
return true;
}
public bool CanExtractSubtitles(string codec)
{
return true;
}
}
}

@ -1,5 +1,3 @@
#nullable disable
using System;
using MediaBrowser.Model.Dto;
@ -59,22 +57,22 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets or sets the media sources.
/// </summary>
public MediaSourceInfo[] MediaSources { get; set; }
public MediaSourceInfo[] MediaSources { get; set; } = Array.Empty<MediaSourceInfo>();
/// <summary>
/// Gets or sets the device profile.
/// </summary>
public DeviceProfile Profile { get; set; }
required public DeviceProfile Profile { get; set; }
/// <summary>
/// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
/// </summary>
public string MediaSourceId { get; set; }
public string? MediaSourceId { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
public string DeviceId { get; set; }
public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets an override of supported number of audio channels

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
@ -37,54 +35,37 @@ namespace MediaBrowser.Model.Dlna
_logger = logger;
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> object.</param>
public StreamBuilder(ILogger<StreamBuilder> logger)
: this(new FullTranscoderSupport(), logger)
{
}
/// <summary>
/// Gets the optimal audio stream.
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
public StreamInfo GetOptimalAudioStream(MediaOptions options)
public StreamInfo? GetOptimalAudioStream(MediaOptions options)
{
ValidateMediaOptions(options, false);
var mediaSources = new List<MediaSourceInfo>();
var streams = new List<StreamInfo>();
foreach (var mediaSource in options.MediaSources)
{
if (string.IsNullOrEmpty(options.MediaSourceId) ||
string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
if (!(string.IsNullOrEmpty(options.MediaSourceId)
|| string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)))
{
mediaSources.Add(mediaSource);
continue;
}
}
var streams = new List<StreamInfo>();
foreach (var mediaSourceInfo in mediaSources)
{
StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options);
if (streamInfo is not null)
{
streamInfo.DeviceId = options.DeviceId;
streamInfo.DeviceProfileId = options.Profile.Id;
streams.Add(streamInfo);
}
}
foreach (var stream in streams)
{
stream.DeviceId = options.DeviceId;
stream.DeviceProfileId = options.Profile.Id;
}
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
}
private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
private StreamInfo? GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
{
var playlistItem = new StreamInfo
{
@ -138,7 +119,7 @@ namespace MediaBrowser.Model.Dlna
}
}
TranscodingProfile transcodingProfile = null;
TranscodingProfile? transcodingProfile = null;
foreach (var tcProfile in options.Profile.TranscodingProfiles)
{
if (tcProfile.Type == playlistItem.MediaType
@ -190,15 +171,15 @@ namespace MediaBrowser.Model.Dlna
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
public StreamInfo GetOptimalVideoStream(MediaOptions options)
public StreamInfo? GetOptimalVideoStream(MediaOptions options)
{
ValidateMediaOptions(options, true);
var mediaSources = new List<MediaSourceInfo>();
foreach (var mediaSourceInfo in options.MediaSources)
{
if (string.IsNullOrEmpty(options.MediaSourceId) ||
string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(options.MediaSourceId)
|| string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSources.Add(mediaSourceInfo);
}
@ -223,7 +204,7 @@ namespace MediaBrowser.Model.Dlna
return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
}
private static StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
=> SortMediaSources(streams, maxBitrate).FirstOrDefault();
private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
@ -366,7 +347,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
/// <returns>The the normalized input container.</returns>
public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
{
@ -394,7 +375,7 @@ namespace MediaBrowser.Model.Dlna
return formats[0];
}
private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
{
var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@ -410,7 +391,6 @@ namespace MediaBrowser.Model.Dlna
return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
var playMethods = new List<PlayMethod>();
TranscodeReason transcodeReasons = 0;
// The profile describes what the device supports
@ -449,7 +429,7 @@ namespace MediaBrowser.Model.Dlna
return (directPlayProfile, null, transcodeReasons);
}
private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream? videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
{
var mediaType = videoStream is null ? DlnaProfileType.Audio : DlnaProfileType.Video;
@ -575,7 +555,7 @@ namespace MediaBrowser.Model.Dlna
}
}
private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
var protocol = "http";
@ -587,7 +567,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SubProtocol = protocol;
playlistItem.VideoCodecs = new[] { item.VideoStream.Codec };
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
@ -634,6 +614,12 @@ namespace MediaBrowser.Model.Dlna
var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
TranscodeReason transcodeReasons = 0;
// Force transcode or remux for BD/DVD folders
if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
{
isEligibleForDirectPlay = false;
}
if (bitrateLimitExceeded)
{
transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
@ -646,7 +632,7 @@ namespace MediaBrowser.Model.Dlna
isEligibleForDirectPlay,
isEligibleForDirectStream);
DirectPlayProfile directPlayProfile = null;
DirectPlayProfile? directPlayProfile = null;
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
@ -677,16 +663,16 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream?.Index;
if (audioStream is not null)
{
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec);
}
SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec);
BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile?.Container, directPlayProfile?.VideoCodec, directPlayProfile?.AudioCodec);
}
if (subtitleStream is not null)
{
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null);
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile?.Container, null);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
@ -748,7 +734,14 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
private TranscodingProfile? GetVideoTranscodeProfile(
MediaSourceInfo item,
MediaOptions options,
MediaStream? videoStream,
MediaStream? audioStream,
IEnumerable<MediaStream> candidateAudioStreams,
MediaStream? subtitleStream,
StreamInfo playlistItem)
{
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
@ -795,7 +788,16 @@ namespace MediaBrowser.Model.Dlna
return transcodingProfiles.FirstOrDefault();
}
private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
private void BuildStreamVideoItem(
StreamInfo playlistItem,
MediaOptions options,
MediaSourceInfo item,
MediaStream? videoStream,
MediaStream? audioStream,
IEnumerable<MediaStream> candidateAudioStreams,
string? container,
string? videoCodec,
string? audioCodec)
{
// Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
@ -862,12 +864,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
string videoProfile = videoStream?.Profile;
string videoRangeType = videoStream?.VideoRangeType;
string? videoProfile = videoStream?.Profile;
string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
string videoCodecTag = videoStream?.CodecTag;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
@ -903,11 +905,11 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
int? inputAudioBitrate = audioStream is null ? null : audioStream.BitRate;
int? audioChannels = audioStream is null ? null : audioStream.Channels;
string audioProfile = audioStream is null ? null : audioStream.Profile;
int? inputAudioSampleRate = audioStream is null ? null : audioStream.SampleRate;
int? inputAudioBitDepth = audioStream is null ? null : audioStream.BitDepth;
int? inputAudioBitrate = audioStream?.BitRate;
int? audioChannels = audioStream?.Channels;
string? audioProfile = audioStream?.Profile;
int? inputAudioSampleRate = audioStream?.SampleRate;
int? inputAudioBitDepth = audioStream?.BitDepth;
var appliedAudioConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
@ -955,7 +957,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem?.TranscodeReasons);
}
private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
{
if (!string.IsNullOrEmpty(audioCodec))
{
@ -988,9 +990,9 @@ namespace MediaBrowser.Model.Dlna
return 192000;
}
private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item)
{
string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
@ -1081,13 +1083,13 @@ namespace MediaBrowser.Model.Dlna
return 7168000;
}
private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
MediaOptions options,
MediaSourceInfo mediaSource,
MediaStream videoStream,
MediaStream audioStream,
MediaStream? videoStream,
MediaStream? audioStream,
ICollection<MediaStream> candidateAudioStreams,
MediaStream subtitleStream,
MediaStream? subtitleStream,
bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
{
@ -1110,12 +1112,12 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth = videoStream?.BitDepth;
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
string videoProfile = videoStream?.Profile;
string videoRangeType = videoStream?.VideoRangeType;
string? videoProfile = videoStream?.Profile;
string? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
string videoCodecTag = videoStream?.CodecTag;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
@ -1203,14 +1205,14 @@ namespace MediaBrowser.Model.Dlna
}
// Check video codec
string videoCodec = videoStream?.Codec;
string? videoCodec = videoStream?.Codec;
if (!directPlayProfile.SupportsVideoCodec(videoCodec))
{
directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
}
// Check audio codec
MediaStream selectedAudioStream = null;
MediaStream? selectedAudioStream = null;
if (candidateAudioStreams.Any())
{
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
@ -1331,8 +1333,8 @@ namespace MediaBrowser.Model.Dlna
SubtitleProfile[] subtitleProfiles,
PlayMethod playMethod,
ITranscoderSupport transcoderSupport,
string outputContainer,
string transcodingSubProtocol)
string? outputContainer,
string? transcodingSubProtocol)
{
if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
{
@ -1405,7 +1407,7 @@ namespace MediaBrowser.Model.Dlna
};
}
private static bool IsSubtitleEmbedSupported(string transcodingContainer)
private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
{
if (!string.IsNullOrEmpty(transcodingContainer))
{
@ -1427,7 +1429,7 @@ namespace MediaBrowser.Model.Dlna
return false;
}
private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
{
foreach (var profile in subtitleProfiles)
{
@ -1560,7 +1562,7 @@ namespace MediaBrowser.Model.Dlna
private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
IEnumerable<CodecProfile> codecProfiles,
string container,
string codec,
string? codec,
int? audioChannels,
int? audioBitrate,
int? audioSampleRate,
@ -1580,7 +1582,7 @@ namespace MediaBrowser.Model.Dlna
return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth));
}
private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
{
foreach (ProfileCondition condition in conditions)
{
@ -2056,7 +2058,7 @@ namespace MediaBrowser.Model.Dlna
}
// Check audio codec
string audioCodec = audioStream?.Codec;
string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
{
return false;

@ -107,9 +107,8 @@ namespace MediaBrowser.Model.Dlna
public string MediaSourceId => MediaSource?.Id;
public bool IsDirectStream =>
PlayMethod == PlayMethod.DirectStream ||
PlayMethod == PlayMethod.DirectPlay;
public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
&& PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
/// <summary>
/// Gets the audio stream that will be used.

@ -30,8 +30,9 @@ namespace MediaBrowser.Model.Globalization
/// Gets the rating level.
/// </summary>
/// <param name="rating">The rating.</param>
/// <param name="countryCode">The optional two letter ISO language string.</param>
/// <returns><see cref="int" /> or <c>null</c>.</returns>
int? GetRatingLevel(string rating);
int? GetRatingLevel(string rating, string? countryCode = null);
/// <summary>
/// Gets the localized string.

@ -0,0 +1,41 @@
#nullable disable
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.MediaInfo;
/// <summary>
/// Represents the result of BDInfo output.
/// </summary>
public class BlurayDiscInfo
{
/// <summary>
/// Gets or sets the media streams.
/// </summary>
/// <value>The media streams.</value>
public MediaStream[] MediaStreams { get; set; }
/// <summary>
/// Gets or sets the run time ticks.
/// </summary>
/// <value>The run time ticks.</value>
public long? RunTimeTicks { get; set; }
/// <summary>
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public string[] Files { get; set; }
/// <summary>
/// Gets or sets the playlist name.
/// </summary>
/// <value>The playlist name.</value>
public string PlaylistName { get; set; }
/// <summary>
/// Gets or sets the chapters.
/// </summary>
/// <value>The chapters.</value>
public double[] Chapters { get; set; }
}

@ -0,0 +1,14 @@
namespace MediaBrowser.Model.MediaInfo;
/// <summary>
/// Interface IBlurayExaminer.
/// </summary>
public interface IBlurayExaminer
{
/// <summary>
/// Gets the disc info.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BlurayDiscInfo.</returns>
BlurayDiscInfo GetDiscInfo(string path);
}

@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ILogger<FFProbeVideoInfo> _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly IBlurayExaminer _blurayExaminer;
private readonly ILocalizationManager _localization;
private readonly IEncodingManager _encodingManager;
private readonly IServerConfigurationManager _config;
@ -51,6 +52,7 @@ namespace MediaBrowser.Providers.MediaInfo
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IEncodingManager encodingManager,
IServerConfigurationManager config,
@ -64,6 +66,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
_localization = localization;
_encodingManager = encodingManager;
_config = config;
@ -80,16 +83,77 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
where T : Video
{
BlurayDiscInfo blurayDiscInfo = null;
Model.MediaInfo.MediaInfo mediaInfoResult = null;
if (!item.IsShortcut || options.EnableRemoteContentProbe)
{
mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
if (item.VideoType == VideoType.Dvd)
{
// Get list of playable .vob files
var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null);
// Return if no playable .vob files are found
if (vobs.Count == 0)
{
_logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe.");
return ItemUpdateType.MetadataImport;
}
// Fetch metadata of first .vob file
mediaInfoResult = await GetMediaInfo(
new Video
{
Path = vobs[0]
},
cancellationToken).ConfigureAwait(false);
// Sum up the runtime of all .vob files skipping the first .vob
for (var i = 1; i < vobs.Count; i++)
{
var tmpMediaInfo = await GetMediaInfo(
new Video
{
Path = vobs[i]
},
cancellationToken).ConfigureAwait(false);
mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks;
}
}
else if (item.VideoType == VideoType.BluRay)
{
// Get BD disc information
blurayDiscInfo = GetBDInfo(item.Path);
// Get playable .m2ts files
var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path);
// Return if no playable .m2ts files are found
if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
{
_logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe.");
return ItemUpdateType.MetadataImport;
}
// Fetch metadata of first .m2ts file
mediaInfoResult = await GetMediaInfo(
new Video
{
Path = m2ts[0]
},
cancellationToken).ConfigureAwait(false);
}
else
{
mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
}
await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false);
await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false);
return ItemUpdateType.MetadataImport;
}
@ -129,6 +193,7 @@ namespace MediaBrowser.Providers.MediaInfo
Video video,
CancellationToken cancellationToken,
Model.MediaInfo.MediaInfo mediaInfo,
BlurayDiscInfo blurayInfo,
MetadataRefreshOptions options)
{
List<MediaStream> mediaStreams;
@ -153,19 +218,8 @@ namespace MediaBrowser.Providers.MediaInfo
}
mediaAttachments = mediaInfo.MediaAttachments;
video.TotalBitrate = mediaInfo.Bitrate;
// video.FormatName = (mediaInfo.Container ?? string.Empty)
// .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
// For DVDs this may not always be accurate, so don't set the runtime if the item already has one
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0;
if (needToSetRuntime)
{
video.RunTimeTicks = mediaInfo.RunTimeTicks;
}
video.RunTimeTicks = mediaInfo.RunTimeTicks;
video.Size = mediaInfo.Size;
if (video.VideoType == VideoType.VideoFile)
@ -182,6 +236,10 @@ namespace MediaBrowser.Providers.MediaInfo
video.Container = mediaInfo.Container;
chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
if (blurayInfo is not null)
{
FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
}
}
else
{
@ -240,7 +298,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
{
if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
chapters = CreateDummyChapters(video);
}
@ -277,6 +335,86 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
{
if (blurayInfo.Files.Length <= 1)
{
return;
}
// Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output
int? currentHeight = null;
int? currentWidth = null;
int? currentBitRate = null;
var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
// Grab the values that ffprobe recorded
if (videoStream is not null)
{
currentBitRate = videoStream.BitRate;
currentWidth = videoStream.Width;
currentHeight = videoStream.Height;
}
// Fill video properties from the BDInfo result
mediaStreams.Clear();
mediaStreams.AddRange(blurayInfo.MediaStreams);
if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
{
video.RunTimeTicks = blurayInfo.RunTimeTicks;
}
if (blurayInfo.Chapters is not null)
{
double[] brChapter = blurayInfo.Chapters;
chapters = new ChapterInfo[brChapter.Length];
for (int i = 0; i < brChapter.Length; i++)
{
chapters[i] = new ChapterInfo
{
StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
};
}
}
videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
// Use the ffprobe values if these are empty
if (videoStream is not null)
{
videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
}
}
private bool IsEmpty(int? num)
{
return !num.HasValue || num.Value == 0;
}
/// <summary>
/// Gets information about the longest playlist on a bdrom.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoStream.</returns>
private BlurayDiscInfo GetBDInfo(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
try
{
return _blurayExaminer.GetDiscInfo(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting BDInfo");
return null;
}
}
private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
{
var replaceData = refreshOptions.ReplaceAllMetadata;
@ -524,39 +662,39 @@ namespace MediaBrowser.Providers.MediaInfo
private ChapterInfo[] CreateDummyChapters(Video video)
{
var runtime = video.RunTimeTicks ?? 0;
long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
if (runtime < 0)
// Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"{0} has invalid runtime of {1}",
"{0} has an invalid runtime of {1} minutes",
video.Name,
runtime));
TimeSpan.FromTicks(runtime).Minutes));
}
if (runtime < dummyChapterDuration)
long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
if (runtime > dummyChapterDuration)
{
return Array.Empty<ChapterInfo>();
}
// Limit the chapters just in case there's some incorrect metadata here
int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount);
var chapters = new ChapterInfo[chapterCount];
int chapterCount = (int)(runtime / dummyChapterDuration);
var chapters = new ChapterInfo[chapterCount];
long currentChapterTicks = 0;
for (int i = 0; i < chapterCount; i++)
{
chapters[i] = new ChapterInfo
long currentChapterTicks = 0;
for (int i = 0; i < chapterCount; i++)
{
StartPositionTicks = currentChapterTicks
};
chapters[i] = new ChapterInfo
{
StartPositionTicks = currentChapterTicks
};
currentChapterTicks += dummyChapterDuration;
}
currentChapterTicks += dummyChapterDuration;
return chapters;
}
return chapters;
return Array.Empty<ChapterInfo>();
}
}
}

@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <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="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
@ -66,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IEncodingManager encodingManager,
IServerConfigurationManager config,
@ -85,6 +87,7 @@ namespace MediaBrowser.Providers.MediaInfo
mediaSourceManager,
mediaEncoder,
itemRepo,
blurayExaminer,
localization,
encodingManager,
config,

@ -53,7 +53,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
return new ImageType[]
{
ImageType.Thumb
};

@ -274,16 +274,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
item.DateCreated = added;
}
else
{
Logger.LogWarning("Invalid Added value found: {Value}", val);
}
item.DateCreated = added;
}
else
{
Logger.LogWarning("Invalid Added value found: {Value}", val);
}
break;
@ -376,15 +373,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "playcount":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)
&& Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
{
var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
userData = _userDataManager.GetUserData(user, item);
userData.PlayCount = count;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
var user = _userManager.GetUserById(guid);
userData = _userDataManager.GetUserData(user, item);
userData.PlayCount = count;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
break;
@ -393,11 +388,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "lastplayed":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
if (Guid.TryParse(nfoConfiguration.UserId, out var guid))
{
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
var user = _userManager.GetUserById(guid);
userData = _userDataManager.GetUserData(user, item);
userData.LastPlayedDate = added;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
@ -487,12 +482,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var text = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(text))
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
break;
@ -630,13 +622,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
var hasDisplayOrder = item as IHasDisplayOrder;
if (hasDisplayOrder is not null)
if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val))
{
if (!string.IsNullOrWhiteSpace(val))
{
hasDisplayOrder.DisplayOrder = val;
}
hasDisplayOrder.DisplayOrder = val;
}
break;
@ -646,12 +634,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (int.TryParse(val, out var productionYear) && productionYear > 1850)
{
if (int.TryParse(val, out var productionYear) && productionYear > 1850)
{
item.ProductionYear = productionYear;
}
item.ProductionYear = productionYear;
}
break;
@ -661,13 +646,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var rating = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(rating))
// All external meta is saving this as '.' for decimal I believe...but just to be sure
if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
{
// All external meta is saving this as '.' for decimal I believe...but just to be sure
if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
{
item.CommunityRating = val;
}
item.CommunityRating = val;
}
break;
@ -697,13 +679,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
item.PremiereDate = date;
item.ProductionYear = date.Year;
}
item.PremiereDate = date;
item.ProductionYear = date.Year;
}
break;
@ -715,12 +694,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
item.EndDate = date;
}
item.EndDate = date;
}
break;
@ -1191,21 +1167,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "value":
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
{
if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
// if ratingName contains tomato --> assume critic rating
if (ratingName is not null
&& ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase)
&& !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
{
// if ratingName contains tomato --> assume critic rating
if (ratingName is not null &&
ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) &&
!ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
if (!ratingName.Contains("avg", StringComparison.OrdinalIgnoreCase))
{
item.CriticRating = ratingValue;
}
else
{
item.CommunityRating = ratingValue;
}
}
else
{
item.CommunityRating = ratingValue;
}
}
@ -1289,12 +1265,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
sortOrder = intVal;
}
break;

@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/bda88810-e1a6-4cf0-8139-7fd7fe7b2c7a/7a9ffa3e12e5f1c3d8b640e326c1eb14/dotnet-sdk-7.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

@ -17,6 +17,8 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
@ -36,6 +38,8 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));

@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1)
configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi
libavutil 58. 2.100 / 58. 2.100
libavcodec 60. 3.100 / 60. 3.100
libavformat 60. 3.100 / 60. 3.100
libavdevice 60. 1.100 / 60. 1.100
libavfilter 9. 3.100 / 9. 3.100
libswscale 7. 1.100 / 7. 1.100
libswresample 4. 10.100 / 4. 10.100
libpostproc 57. 1.100 / 57. 1.100";
public const string FFmpegV512Output = @"ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 10-win32 (GCC) 20220324
configuration: --prefix=/opt/ffmpeg --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --pkg-config-flags=--static --extra-libs='-lfftw3f -lstdc++' --extra-cflags=-DCHROMAPRINT_NODLL --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --disable-w32threads --enable-pthreads --enable-shared --enable-lto --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
libavutil 57. 28.100 / 57. 28.100
libavcodec 59. 37.100 / 59. 37.100
libavformat 59. 27.100 / 59. 27.100
libavdevice 59. 7.100 / 59. 7.100
libavfilter 8. 44.100 / 8. 44.100
libswscale 6. 7.100 / 6. 7.100
libswresample 4. 7.100 / 4. 7.100
libpostproc 56. 6.100 / 56. 6.100";
public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10.3.0 (Rev5, Built by MSYS2 project)
configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls

@ -0,0 +1,76 @@
using System.Linq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.Audio;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class AudioResolverTests
{
private static readonly NamingOptions _namingOptions = new();
[Theory]
[InlineData("words.mp3")] // single non-tagged file
[InlineData("chapter 01.mp3")]
[InlineData("part 1.mp3")]
[InlineData("chapter 01.mp3", "non-media.txt")]
[InlineData("title.mp3", "title.epub")]
[InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory
public void Resolve_AudiobookDirectory_SingleResult(params string[] children)
{
var resolved = TestResolveChildren("/parent/title", children);
Assert.NotNull(resolved);
}
[Theory]
/* Results that can't be displayed as an audio book. */
[InlineData] // no contents
[InlineData("subdirectory/")]
[InlineData("non-media.txt")]
/* Names don't indicate parts of a single book. */
[InlineData("Name.mp3", "Another Name.mp3")]
/* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */
[InlineData("01.mp3", "02.mp3")]
[InlineData("chapter 01.mp3", "chapter 02.mp3")]
[InlineData("part 1.mp3", "part 2.mp3")]
[InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")]
/* Mismatched chapters, parts, and named files. */
[InlineData("chapter 01.mp3", "part 2.mp3")]
[InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name
[InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1
[InlineData("Chapter Name.mp3", "Part 1.mp3")]
public void Resolve_AudiobookDirectory_NoResult(params string[] children)
{
var resolved = TestResolveChildren("/parent/book title", children);
Assert.Null(resolved);
}
private Audio? TestResolveChildren(string parent, string[] children)
{
var childrenMetadata = children.Select(name => new FileSystemMetadata
{
FullName = parent + "/" + name,
IsDirectory = name.EndsWith('/')
}).ToArray();
var resolver = new AudioResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
null,
Mock.Of<ILibraryManager>())
{
CollectionType = "books",
FileInfo = new FileSystemMetadata
{
FullName = parent,
IsDirectory = true
},
FileSystemChildren = childrenMetadata
};
return resolver.Resolve(itemResolveArgs);
}
}

@ -22,10 +22,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
var parent = new Folder { Name = "extras" };
var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
null)
{
Parent = parent,
CollectionType = CollectionType.TvShows,
@ -45,10 +45,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions);
var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
null)
{
Parent = series,
CollectionType = CollectionType.TvShows,
@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private sealed class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions)
public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService)
{
}

@ -18,10 +18,10 @@ public class MovieResolverTests
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions);
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
null)
{
Parent = null,
FileInfo = new FileSystemMetadata

@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(53, ratings.Count);
Assert.Equal(54, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
@ -100,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(18, ratings.Count);
Assert.Equal(19, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);

Loading…
Cancel
Save