mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 09:59:06 -07:00
Merge branch 'master' into network-rewrite
This commit is contained in:
commit
520c07e8ca
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@ -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
|
||||
|
4
.github/workflows/openapi.yml
vendored
4
.github/workflows/openapi.yml
vendored
@ -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 name switch
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Play)
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (name == SessionMessageType.Playstate)
|
||||
{
|
||||
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);
|
||||
|
||||
return folders.Cast<BaseItem>().ToList();
|
||||
folders.AddRange(channels.Items);
|
||||
|
||||
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}'");
|
||||
}
|
||||
if (_allParentalRatings.TryGetValue(countryCode, out var countryValue))
|
||||
{
|
||||
return countryValue;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
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))
|
||||
{
|
||||
return value.Value;
|
||||
var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
|
||||
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't find anything check all ratings systems
|
||||
foreach (var dictionary in _allParentalRatings.Values)
|
||||
else
|
||||
{
|
||||
if (dictionary.TryGetValue(rating, out 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Try splitting by : to handle "Germany: FSK 18"
|
||||
// If we don't find anything, check all ratings systems
|
||||
foreach (var dictionary in _allParentalRatings.Values)
|
||||
{
|
||||
if (dictionary.TryGetValue(rating, out var value))
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
|
123
MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
Normal file
123
MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
187
MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
Normal file
187
MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
Normal file
@ -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
|
||||
});
|
||||
}
|
||||
}
|
68
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
Normal file
68
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
Normal file
@ -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.
|
||||
|
41
MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
Normal file
41
MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
Normal file
@ -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; }
|
||||
}
|
14
MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
Normal file
14
MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
Normal file
@ -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>();
|
||||
}
|
||||
int chapterCount = (int)(runtime / dummyChapterDuration);
|
||||
var chapters = new ChapterInfo[chapterCount];
|
||||
|
||||
// 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];
|
||||
|
||||
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…
Reference in New Issue
Block a user