mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 08:08:16 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf78aefbb7 | ||
|
|
344cc8b97b | ||
|
|
cc9c000412 | ||
|
|
5c6317f68d | ||
|
|
80940c0c57 | ||
|
|
8aa41d5904 | ||
|
|
cea0c95942 | ||
|
|
4e28f4fe03 | ||
|
|
f0e9b2fb96 | ||
|
|
b9881b8bdf | ||
|
|
b31f1696f2 | ||
|
|
86160cd99c | ||
|
|
230eacf15e | ||
|
|
0ecaa98ee7 | ||
|
|
45c4bedbc6 | ||
|
|
2c4c1d054d | ||
|
|
f97f38585b | ||
|
|
a2a0cbf7ab | ||
|
|
eb5f8d49dd | ||
|
|
6f7ce439d3 | ||
|
|
03ea566271 | ||
|
|
2a96b8b34b | ||
|
|
ff4f3b0441 | ||
|
|
d49bb1d86d | ||
|
|
cf6aa12627 | ||
|
|
cd4519c15f | ||
|
|
8e248c7c05 | ||
|
|
65f722f23c | ||
|
|
e7ac3e3929 | ||
|
|
9464f9e622 | ||
|
|
746280af0b | ||
|
|
9bc6e8a306 | ||
|
|
b0105179eb |
@@ -51,7 +51,7 @@
|
||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||
<PackageVersion Include="NEbml" Version="0.12.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
@@ -80,7 +80,7 @@
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.8.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.11.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
@@ -88,4 +88,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
||||
{
|
||||
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
||||
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
|
||||
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
|
||||
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
|
||||
&& (videoIsDoVi && clientSupportsDoVi))
|
||||
{
|
||||
if (isActualOutputVideoCodecHevc)
|
||||
{
|
||||
|
||||
@@ -235,6 +235,11 @@ public static class StreamingHelpers
|
||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
|
||||
}
|
||||
}
|
||||
|
||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
|
||||
return;
|
||||
}
|
||||
|
||||
// We support video backdrops, but we should not generate trickplay images for them
|
||||
var parentDirectory = Directory.GetParent(mediaPath);
|
||||
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
||||
var actualWidth = 2 * (width / 2);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -309,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream is null
|
||||
|| !options.EnableTonemapping
|
||||
|| GetVideoColorBitDepth(state) < 10
|
||||
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
||||
{
|
||||
@@ -2061,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// libx265 only accept level option in -x265-params.
|
||||
// level option may cause libx265 to fail.
|
||||
// libx265 cannot adjust the given level, just throw an error.
|
||||
param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
|
||||
param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
|
||||
|
||||
if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
|
||||
{
|
||||
// The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
|
||||
param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -5690,7 +5695,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
if (!string.IsNullOrEmpty(doScaling)
|
||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||
{
|
||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
|
||||
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||
// Use NV15 instead of P010 to avoid the issue.
|
||||
// SDR inputs are using BGRA formats already which is not affected.
|
||||
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
@@ -7064,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// DTS and TrueHD are not supported by HLS
|
||||
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
||||
shiftAudioCodecs.Add("dca");
|
||||
shiftAudioCodecs.Add("dts");
|
||||
shiftAudioCodecs.Add("truehd");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -246,7 +246,7 @@ public class ServerConfiguration : BaseApplicationConfiguration
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||
/// </summary>
|
||||
public bool RemoveOldPlugins { get; set; }
|
||||
public bool RemoveOldPlugins { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
private readonly ITranscoderSupport _transcoderSupport;
|
||||
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
||||
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
|
||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
|
||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
||||
@@ -862,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (options.AllowAudioStreamCopy)
|
||||
{
|
||||
if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
|
||||
// For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
|
||||
// the transcoding conditions, then the one does not satisfy the transcoding conditions.
|
||||
// For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
|
||||
// When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
|
||||
var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
|
||||
|
||||
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
|
||||
{
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||
i.ContainsAnyCodec(audioCodec, container) &&
|
||||
i.ContainsAnyCodec(transcodingAudioCodec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
|
||||
.Select(i =>
|
||||
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
|
||||
|
||||
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
|
||||
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
||||
rank.Audio = conditionsSatisfied ? 1 : 2;
|
||||
|
||||
var rankAudio = 3;
|
||||
|
||||
if (conditionsSatisfied)
|
||||
{
|
||||
rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
|
||||
}
|
||||
|
||||
rank.Audio = Math.Min(rank.Audio, rankAudio);
|
||||
|
||||
if (rank.Audio == 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,9 +982,26 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
|
||||
|
||||
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
|
||||
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
|
||||
|
||||
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
|
||||
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
|
||||
&& options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.VideoAudio
|
||||
&& i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
|
||||
&& i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
|
||||
.Select(i => i.Conditions.All(condition =>
|
||||
{
|
||||
var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
|
||||
if (!satisfied)
|
||||
{
|
||||
playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
|
||||
}
|
||||
|
||||
return satisfied;
|
||||
}))
|
||||
.All(satisfied => satisfied);
|
||||
|
||||
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
|
||||
|
||||
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
||||
{
|
||||
@@ -2213,7 +2249,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
||||
private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
|
||||
{
|
||||
// Check container type
|
||||
if (!profile.SupportsContainer(item.Container))
|
||||
@@ -2221,6 +2257,20 @@ namespace MediaBrowser.Model.Dlna
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never direct play audio in matroska when the device only declare support for webm.
|
||||
// The first check is not enough because mkv is assumed can be webm.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/13344
|
||||
return !ContainerHelper.ContainsContainer("mkv", item.Container)
|
||||
|| profile.SupportsContainer("mkv");
|
||||
}
|
||||
|
||||
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
||||
{
|
||||
if (!IsAudioContainerSupported(profile, item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check audio codec
|
||||
string? audioCodec = audioStream?.Codec;
|
||||
if (!profile.SupportsAudioCodec(audioCodec))
|
||||
@@ -2235,19 +2285,16 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
// Check container type, this should NOT be supported
|
||||
// If the container is supported, the file should be directly played
|
||||
if (!profile.SupportsContainer(item.Container))
|
||||
if (IsAudioContainerSupported(profile, item))
|
||||
{
|
||||
// Check audio codec, we cannot use the SupportsAudioCodec here
|
||||
// Because that one assumes empty container supports all codec, which is just useless
|
||||
string? audioCodec = audioStream?.Codec;
|
||||
if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Check audio codec, we cannot use the SupportsAudioCodec here
|
||||
// Because that one assumes empty container supports all codec, which is just useless
|
||||
string? audioCodec = audioStream?.Codec;
|
||||
return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
|
||||
|
||||
/// <summary>
|
||||
/// A class for transcoding profile information.
|
||||
/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
|
||||
/// </summary>
|
||||
public class TranscodingProfile
|
||||
{
|
||||
@@ -17,6 +19,33 @@ public class TranscodingProfile
|
||||
Conditions = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
|
||||
/// </summary>
|
||||
/// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
|
||||
public TranscodingProfile(TranscodingProfile other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
Container = other.Container;
|
||||
Type = other.Type;
|
||||
VideoCodec = other.VideoCodec;
|
||||
AudioCodec = other.AudioCodec;
|
||||
Protocol = other.Protocol;
|
||||
EstimateContentLength = other.EstimateContentLength;
|
||||
EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
|
||||
TranscodeSeekInfo = other.TranscodeSeekInfo;
|
||||
CopyTimestamps = other.CopyTimestamps;
|
||||
Context = other.Context;
|
||||
EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
|
||||
MaxAudioChannels = other.MaxAudioChannels;
|
||||
MinSegments = other.MinSegments;
|
||||
SegmentLength = other.SegmentLength;
|
||||
BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
|
||||
Conditions = other.Conditions;
|
||||
EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
try
|
||||
{
|
||||
var fileStream = AsyncFile.OpenRead(source);
|
||||
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
|
||||
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
var people = new List<PersonInfo>();
|
||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
|
||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
@@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (performers is null || performers.Length == 0)
|
||||
{
|
||||
performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
|
||||
performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
|
||||
}
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
@@ -314,7 +314,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||
{
|
||||
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
|
||||
@@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
private void RemoveObsoleteEpisodes(Series series)
|
||||
{
|
||||
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
||||
var numberOfEpisodes = episodes.Count;
|
||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
||||
for (var i = 0; i < numberOfEpisodes; i++)
|
||||
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
|
||||
.OfType<Episode>()
|
||||
.GroupBy(e => e.ParentIndexNumber)
|
||||
.ToList();
|
||||
|
||||
foreach (var seasonEpisodes in episodesBySeason)
|
||||
{
|
||||
var currentEpisode = episodes[i];
|
||||
// The outer loop only examines virtual episodes
|
||||
if (!currentEpisode.IsVirtualItem)
|
||||
List<Episode> nonPhysicalEpisodes = [];
|
||||
List<Episode> physicalEpisodes = [];
|
||||
foreach (var episode in seasonEpisodes)
|
||||
{
|
||||
continue;
|
||||
if (episode.IsVirtualItem || episode.IsMissingEpisode)
|
||||
{
|
||||
nonPhysicalEpisodes.Add(episode);
|
||||
continue;
|
||||
}
|
||||
|
||||
physicalEpisodes.Add(episode);
|
||||
}
|
||||
|
||||
// Virtual episodes without an episode number are practically orphaned and should be deleted
|
||||
if (!currentEpisode.IndexNumber.HasValue)
|
||||
// Only consider non-physical episodes
|
||||
foreach (var episode in nonPhysicalEpisodes)
|
||||
{
|
||||
DeleteEpisode(currentEpisode);
|
||||
continue;
|
||||
}
|
||||
// Episodes without an episode number are practically orphaned and should be deleted
|
||||
// Episodes with a physical equivalent should be deleted (they are no longer missing)
|
||||
var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
|
||||
|
||||
for (var j = i + 1; j < numberOfEpisodes; j++)
|
||||
{
|
||||
var comparisonEpisode = episodes[j];
|
||||
// The inner loop is only for "physical" episodes
|
||||
if (comparisonEpisode.IsVirtualItem
|
||||
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|
||||
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
|
||||
if (shouldKeep)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DeleteEpisode(currentEpisode);
|
||||
break;
|
||||
DeleteEpisode(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
{
|
||||
case "id":
|
||||
{
|
||||
// get ids from attributes
|
||||
// Get ids from attributes
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||
string? imdbId = reader.GetAttribute("IMDB");
|
||||
string? tmdbId = reader.GetAttribute("TMDB");
|
||||
|
||||
// read id from content
|
||||
// Read id from content
|
||||
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||
var contentId = reader.ReadElementContentAsString();
|
||||
if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
|
||||
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||
{
|
||||
imdbId = contentId;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
tmdbId = contentId;
|
||||
}
|
||||
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using Emby.Naming.TV;
|
||||
@@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
{
|
||||
case "id":
|
||||
{
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
|
||||
// Get ids from attributes
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||
string? imdbId = reader.GetAttribute("IMDB");
|
||||
|
||||
string? tvdbId = reader.GetAttribute("TVDB");
|
||||
if (string.IsNullOrWhiteSpace(tvdbId))
|
||||
// Read id from content
|
||||
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||
var contentId = reader.ReadElementContentAsString();
|
||||
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||
{
|
||||
tvdbId = reader.ReadElementContentAsString();
|
||||
imdbId = contentId;
|
||||
}
|
||||
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.10.3")]
|
||||
[assembly: AssemblyFileVersion("10.10.3")]
|
||||
[assembly: AssemblyVersion("10.10.4")]
|
||||
[assembly: AssemblyFileVersion("10.10.4")]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.10.3</VersionPrefix>
|
||||
<VersionPrefix>10.10.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities.Libraries;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.LiveTv.Configuration;
|
||||
@@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of days images are pre-cached from external sources.
|
||||
/// </summary>
|
||||
public const int MaxCacheDays = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
||||
/// </summary>
|
||||
@@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
|
||||
progress.Report(15);
|
||||
|
||||
numComplete = 0;
|
||||
var programs = new List<Guid>();
|
||||
var programs = new List<LiveTvProgram>();
|
||||
var channels = new List<Guid>();
|
||||
|
||||
var guideDays = GetGuideDays();
|
||||
|
||||
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
|
||||
_logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
|
||||
|
||||
var maxCacheDate = DateTime.UtcNow.AddDays(2);
|
||||
var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
|
||||
foreach (var currentChannel in list)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
|
||||
DtoOptions = new DtoOptions(true)
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
||||
var newPrograms = new List<LiveTvProgram>();
|
||||
var updatedPrograms = new List<BaseItem>();
|
||||
var newPrograms = new List<Guid>();
|
||||
var updatedPrograms = new List<Guid>();
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
||||
var id = programItem.Id;
|
||||
if (isNew)
|
||||
{
|
||||
newPrograms.Add(programItem);
|
||||
newPrograms.Add(id);
|
||||
}
|
||||
else if (isUpdated)
|
||||
{
|
||||
updatedPrograms.Add(programItem);
|
||||
updatedPrograms.Add(id);
|
||||
}
|
||||
|
||||
programs.Add(programItem.Id);
|
||||
programs.Add(programItem);
|
||||
|
||||
isMovie |= program.IsMovie;
|
||||
isSeries |= program.IsSeries;
|
||||
@@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
|
||||
isKids |= program.IsKids;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
|
||||
_logger.LogDebug(
|
||||
"Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
|
||||
currentChannel.Name,
|
||||
newPrograms.Count,
|
||||
updatedPrograms.Count);
|
||||
|
||||
if (newPrograms.Count > 0)
|
||||
{
|
||||
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
|
||||
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
|
||||
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
|
||||
}
|
||||
|
||||
if (updatedPrograms.Count > 0)
|
||||
{
|
||||
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
|
||||
await _libraryManager.UpdateItemsAsync(
|
||||
updatedPrograms,
|
||||
updatedProgramDtos,
|
||||
currentChannel,
|
||||
ItemUpdateType.MetadataImport,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
|
||||
|
||||
currentChannel.IsMovie = isMovie;
|
||||
currentChannel.IsNews = isNews;
|
||||
currentChannel.IsSports = isSports;
|
||||
@@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
|
||||
var programIds = programs.Select(p => p.Id).ToList();
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
||||
}
|
||||
|
||||
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
@@ -618,77 +632,17 @@ public class GuideManager : IGuideManager
|
||||
item.IndexNumber = info.EpisodeNumber;
|
||||
item.ParentIndexNumber = info.SeasonNumber;
|
||||
|
||||
if (!item.HasImage(ImageType.Primary))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.ImagePath))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.ImagePath,
|
||||
Type = ImageType.Primary
|
||||
},
|
||||
0);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.ImageUrl,
|
||||
Type = ImageType.Primary
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
forceUpdate = forceUpdate || UpdateImages(item, info);
|
||||
|
||||
if (!item.HasImage(ImageType.Thumb))
|
||||
if (isNew)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.ThumbImageUrl,
|
||||
Type = ImageType.Thumb
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
item.OnMetadataChanged();
|
||||
|
||||
if (!item.HasImage(ImageType.Logo))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.LogoImageUrl,
|
||||
Type = ImageType.Logo
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.HasImage(ImageType.Backdrop))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.BackdropImageUrl,
|
||||
Type = ImageType.Backdrop
|
||||
},
|
||||
0);
|
||||
}
|
||||
return (item, isNew, false);
|
||||
}
|
||||
|
||||
var isUpdated = false;
|
||||
if (isNew)
|
||||
{
|
||||
}
|
||||
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
{
|
||||
isUpdated = true;
|
||||
}
|
||||
@@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew || isUpdated)
|
||||
if (isUpdated)
|
||||
{
|
||||
item.OnMetadataChanged();
|
||||
}
|
||||
@@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
|
||||
return (item, isNew, isUpdated);
|
||||
}
|
||||
|
||||
private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
||||
private static bool UpdateImages(BaseItem item, ProgramInfo info)
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
// Primary
|
||||
updated |= UpdateImage(ImageType.Primary, item, info);
|
||||
|
||||
// Thumbnail
|
||||
updated |= UpdateImage(ImageType.Thumb, item, info);
|
||||
|
||||
// Logo
|
||||
updated |= UpdateImage(ImageType.Logo, item, info);
|
||||
|
||||
// Backdrop
|
||||
return updated || UpdateImage(ImageType.Backdrop, item, info);
|
||||
}
|
||||
|
||||
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
|
||||
{
|
||||
var image = item.GetImages(imageType).FirstOrDefault();
|
||||
var currentImagePath = image?.Path;
|
||||
var newImagePath = imageType switch
|
||||
{
|
||||
ImageType.Primary => info.ImagePath,
|
||||
_ => string.Empty
|
||||
};
|
||||
var newImageUrl = imageType switch
|
||||
{
|
||||
ImageType.Backdrop => info.BackdropImageUrl,
|
||||
ImageType.Logo => info.LogoImageUrl,
|
||||
ImageType.Primary => info.ImageUrl,
|
||||
ImageType.Thumb => info.ThumbImageUrl,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
|
||||
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
|
||||
if (!differentImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newImagePath))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = newImagePath,
|
||||
Type = imageType
|
||||
},
|
||||
0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = newImageUrl,
|
||||
Type = imageType
|
||||
},
|
||||
0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
item.RemoveImage(image);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
||||
{
|
||||
await Parallel.ForEachAsync(
|
||||
programs
|
||||
@@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
|
||||
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.LiveTv.Guide;
|
||||
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
||||
|
||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private DateTime _lastErrorResponse;
|
||||
private bool _disposed = false;
|
||||
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
{
|
||||
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
||||
|
||||
return Enumerable.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
||||
var requestList = new List<RequestScheduleForChannelDto>()
|
||||
{
|
||||
new RequestScheduleForChannelDto()
|
||||
new()
|
||||
{
|
||||
StationId = channelId,
|
||||
Date = dates
|
||||
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
if (dailySchedules is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
||||
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
|
||||
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||
|
||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||
if (programDetails is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
||||
|
||||
var programIdsWithImages = programDetails
|
||||
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
|
||||
.Where(p => p.HasImageArtwork)
|
||||
.Select(p => p.ProgramId)
|
||||
.ToList();
|
||||
|
||||
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
||||
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
|
||||
var programsInfo = new List<ProgramInfo>();
|
||||
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
||||
{
|
||||
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
|
||||
// " which corresponds to channel " + channelNumber + " and program id " +
|
||||
// schedule.ProgramId + " which says it has images? " +
|
||||
// programDict[schedule.ProgramId].hasImageArtwork);
|
||||
|
||||
if (string.IsNullOrEmpty(schedule.ProgramId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (images is not null)
|
||||
// Only add images which will be pre-cached until we can implement dynamic token fetching
|
||||
var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
|
||||
var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
|
||||
if (willBeCached && images is not null)
|
||||
{
|
||||
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
||||
if (imageIndex > -1)
|
||||
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
return [];
|
||||
}
|
||||
|
||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
{
|
||||
_logger.LogError(ex, "Error getting image info from schedules direct");
|
||||
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
|
||||
else if (!IsInLocalNetwork(remoteIP))
|
||||
{
|
||||
// Remote not enabled. So everyone should be LAN.
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user