Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
322d6cc65d | ||
|
|
c208a81b75 | ||
|
|
5176582391 | ||
|
|
e518b79c36 |
@@ -3,8 +3,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
|
||||||
<AssemblyVersion>1.0.0.9</AssemblyVersion>
|
<AssemblyVersion>1.0.0.11</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.9</FileVersion>
|
<FileVersion>1.0.0.11</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.0" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.11.0" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.11.0" />
|
<PackageReference Include="Jellyfin.Model" Version="10.11.0" />
|
||||||
<PackageReference Include="YoutubeExplode" Version="6.*" />
|
<PackageReference Include="YoutubeExplode" Version="6.6.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
|
|
||||||
var paths = BuildMediaPaths(movie.Title, movie.Year, config);
|
var paths = BuildMediaPaths(movie.Title, movie.Year, config);
|
||||||
|
|
||||||
if (config.SkipAlreadyDownloaded && Directory.Exists(paths.MediaFolder))
|
if (config.SkipAlreadyDownloaded && File.Exists(paths.TrailerPath))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("|CinemaTrailers4Jellyfins| Already downloaded: {Path}", paths.MediaFolder);
|
_logger.LogDebug("|CinemaTrailers4Jellyfins| Already downloaded: {Path}", paths.MediaFolder);
|
||||||
continue;
|
continue;
|
||||||
@@ -197,7 +197,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
|
{
|
||||||
|
// Don't leave an empty folder behind: it would count toward MaxTotalTrailers
|
||||||
|
// (pushing out real trailers) and would never be retried.
|
||||||
|
TryDeleteFolder(paths.MediaFolder);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var fakeMovieReady = await _fakeMovieService.CopyFakeMovieAsync(paths.FakeMoviePath, cancellationToken).ConfigureAwait(false);
|
var fakeMovieReady = await _fakeMovieService.CopyFakeMovieAsync(paths.FakeMoviePath, cancellationToken).ConfigureAwait(false);
|
||||||
if (!fakeMovieReady)
|
if (!fakeMovieReady)
|
||||||
@@ -272,7 +277,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
|
|
||||||
var paths = BuildMediaPaths(show.Title, show.Year, config);
|
var paths = BuildMediaPaths(show.Title, show.Year, config);
|
||||||
|
|
||||||
if (config.SkipAlreadyDownloaded && Directory.Exists(paths.MediaFolder))
|
if (config.SkipAlreadyDownloaded && File.Exists(paths.TrailerPath))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("|CinemaTrailers4Jellyfins| Already downloaded: {Path}", paths.MediaFolder);
|
_logger.LogDebug("|CinemaTrailers4Jellyfins| Already downloaded: {Path}", paths.MediaFolder);
|
||||||
continue;
|
continue;
|
||||||
@@ -298,7 +303,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks
|
|||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
|
{
|
||||||
|
// Don't leave an empty folder behind: it would count toward MaxTotalTrailers
|
||||||
|
// (pushing out real trailers) and would never be retried.
|
||||||
|
TryDeleteFolder(paths.MediaFolder);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var fakeMovieReady = await _fakeMovieService.CopyFakeMovieAsync(paths.FakeMoviePath, cancellationToken).ConfigureAwait(false);
|
var fakeMovieReady = await _fakeMovieService.CopyFakeMovieAsync(paths.FakeMoviePath, cancellationToken).ConfigureAwait(false);
|
||||||
if (!fakeMovieReady)
|
if (!fakeMovieReady)
|
||||||
|
|||||||
@@ -109,16 +109,11 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
"-f mp4",
|
"-f mp4",
|
||||||
$"\"{tempPath}\"");
|
$"\"{tempPath}\"");
|
||||||
|
|
||||||
// Prefer Jellyfin's bundled ffmpeg; fall back to whatever is on PATH.
|
|
||||||
var ffmpegPath = File.Exists("/usr/lib/jellyfin-ffmpeg/ffmpeg")
|
|
||||||
? "/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
|
||||||
: "ffmpeg";
|
|
||||||
|
|
||||||
using var process = new Process
|
using var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = ffmpegPath,
|
FileName = FfmpegLocator.Path,
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
||||||
|
{
|
||||||
|
internal static class FfmpegLocator
|
||||||
|
{
|
||||||
|
private const string JellyfinFfmpeg = "/usr/lib/jellyfin-ffmpeg/ffmpeg";
|
||||||
|
|
||||||
|
// Prefer Jellyfin's bundled ffmpeg; fall back to whatever is on PATH.
|
||||||
|
public static string Path => File.Exists(JellyfinFfmpeg) ? JellyfinFfmpeg : "ffmpeg";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,10 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
int preferredHeight,
|
int preferredHeight,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
// Separate video/audio downloads land next to the output and are always removed,
|
||||||
|
// whether the merge succeeds or not.
|
||||||
|
var videoTemp = outputPath + ".video.tmp";
|
||||||
|
var audioTemp = outputPath + ".audio.tmp";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var youtube = new YoutubeClient(_httpClient);
|
var youtube = new YoutubeClient(_httpClient);
|
||||||
@@ -84,30 +88,42 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
.GetManifestAsync($"https://www.youtube.com/watch?v={key}", ct)
|
.GetManifestAsync($"https://www.youtube.com/watch?v={key}", ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// Muxed streams include audio+video in one file. Quality is capped at 720p by YouTube.
|
// Muxed streams include audio+video in one file. YouTube rarely serves them
|
||||||
|
// anymore, but use one when it exists since it needs no merge.
|
||||||
var muxedStreams = manifest.GetMuxedStreams().ToList();
|
var muxedStreams = manifest.GetMuxedStreams().ToList();
|
||||||
if (muxedStreams.Count == 0)
|
if (muxedStreams.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("|CinemaTrailers4Jellyfins| No muxed streams available for {Key}. Consider configuring yt-dlp for 1080p support.", key);
|
var muxed = PickByHeight(muxedStreams, preferredHeight);
|
||||||
|
_logger.LogInformation(
|
||||||
|
"|CinemaTrailers4Jellyfins| Downloading {Key} at {Quality} (muxed) to {Path}",
|
||||||
|
key, muxed.VideoQuality.Label, outputPath);
|
||||||
|
await youtube.Videos.Streams.DownloadAsync(muxed, outputPath, cancellationToken: ct).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise download the best video-only and audio-only streams and merge them.
|
||||||
|
// MP4 (H.264/AAC) is preferred for client compatibility; any container is the fallback.
|
||||||
|
var videoStreams = manifest.GetVideoOnlyStreams().ToList();
|
||||||
|
var audioStreams = manifest.GetAudioOnlyStreams().ToList();
|
||||||
|
if (videoStreams.Count == 0 || audioStreams.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("|CinemaTrailers4Jellyfins| No usable video/audio streams for {Key}.", key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the highest quality at or below the configured height limit.
|
var mp4Video = videoStreams.Where(s => s.Container == Container.Mp4).ToList();
|
||||||
var stream = muxedStreams
|
var video = PickByHeight(mp4Video.Count > 0 ? mp4Video : videoStreams, preferredHeight);
|
||||||
.Where(s => s.VideoQuality.MaxHeight <= preferredHeight)
|
var mp4Audio = audioStreams.Where(s => s.Container == Container.Mp4).ToList();
|
||||||
.OrderByDescending(s => s.VideoQuality.MaxHeight)
|
var audio = (mp4Audio.Count > 0 ? mp4Audio : audioStreams).GetWithHighestBitrate();
|
||||||
.FirstOrDefault()
|
|
||||||
?? muxedStreams.OrderByDescending(s => s.VideoQuality.MaxHeight).First();
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"|CinemaTrailers4Jellyfins| Downloading {Key} at {Quality} to {Path}",
|
"|CinemaTrailers4Jellyfins| Downloading {Key} at {Quality} (video + audio, merged with ffmpeg) to {Path}",
|
||||||
key, stream.VideoQuality.Label, outputPath);
|
key, video.VideoQuality.Label, outputPath);
|
||||||
|
|
||||||
await youtube.Videos.Streams
|
await youtube.Videos.Streams.DownloadAsync(video, videoTemp, cancellationToken: ct).ConfigureAwait(false);
|
||||||
.DownloadAsync(stream, outputPath, cancellationToken: ct)
|
await youtube.Videos.Streams.DownloadAsync(audio, audioTemp, cancellationToken: ct).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return true;
|
return await MergeAsync(key, videoTemp, audioTemp, outputPath, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -118,6 +134,85 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| YoutubeExplode download failed for {Key}", key);
|
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| YoutubeExplode download failed for {Key}", key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDeleteFile(videoTemp);
|
||||||
|
TryDeleteFile(audioTemp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highest quality at or below the configured height limit, else the lowest available.
|
||||||
|
private static T PickByHeight<T>(System.Collections.Generic.IReadOnlyList<T> streams, int preferredHeight)
|
||||||
|
where T : IVideoStreamInfo
|
||||||
|
=> streams
|
||||||
|
.Where(s => s.VideoQuality.MaxHeight <= preferredHeight)
|
||||||
|
.OrderByDescending(s => s.VideoQuality.MaxHeight)
|
||||||
|
.FirstOrDefault()
|
||||||
|
?? streams.OrderBy(s => s.VideoQuality.MaxHeight).First();
|
||||||
|
|
||||||
|
// Stream-copies (no re-encode) the video and audio tracks into one MP4.
|
||||||
|
// Writes to a temp file first so a failed merge never leaves a broken trailer.
|
||||||
|
private async Task<bool> MergeAsync(string key, string videoPath, string audioPath, string outputPath, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var mergeTemp = outputPath + ".merge.tmp";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var args = string.Join(" ",
|
||||||
|
"-y -loglevel error",
|
||||||
|
$"-i \"{videoPath}\"",
|
||||||
|
$"-i \"{audioPath}\"",
|
||||||
|
"-map 0:v:0 -map 1:a:0 -c copy -movflags +faststart",
|
||||||
|
"-f mp4",
|
||||||
|
$"\"{mergeTemp}\"");
|
||||||
|
|
||||||
|
using var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = FfmpegLocator.Path,
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
// Drain both pipes while ffmpeg runs so a full buffer can never block it.
|
||||||
|
var stdoutTask = process.StandardOutput.ReadToEndAsync(ct);
|
||||||
|
var stderrTask = process.StandardError.ReadToEndAsync(ct);
|
||||||
|
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
||||||
|
await stdoutTask.ConfigureAwait(false);
|
||||||
|
var stderr = await stderrTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (process.ExitCode != 0 || !File.Exists(mergeTemp))
|
||||||
|
{
|
||||||
|
_logger.LogError("|CinemaTrailers4Jellyfins| ffmpeg merge failed for {Key} (exit code {Code}): {Error}",
|
||||||
|
key, process.ExitCode, stderr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(mergeTemp, outputPath, overwrite: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDeleteFile(mergeTemp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "|CinemaTrailers4Jellyfins| Could not delete temporary file {Path}", path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> DownloadWithYtDlpAsync(
|
private async Task<bool> DownloadWithYtDlpAsync(
|
||||||
|
|||||||
@@ -147,9 +147,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Apply enabled filters. If nothing survives, fall back to the full pool.
|
// Animation is a style, not a topic: animated trailers (anime included) only play
|
||||||
var filtered = ApplyFilters(pairs, feature, config);
|
// before animated content. Applied before the fallback below, so a live-action
|
||||||
var pool = filtered.Count > 0 ? filtered : pairs;
|
// feature never gets one — it gets no trailer instead.
|
||||||
|
var eligible = IsAnimated(feature)
|
||||||
|
? pairs
|
||||||
|
: pairs.Where(p => !IsAnimated(p.Movie)).ToList();
|
||||||
|
|
||||||
|
// Apply enabled filters. If nothing survives, fall back to the eligible pool.
|
||||||
|
var filtered = ApplyFilters(eligible, feature, config);
|
||||||
|
var pool = filtered.Count > 0 ? filtered : eligible;
|
||||||
|
|
||||||
// Prefer unseen trailers within the pool; reset the cycle when all have been shown.
|
// Prefer unseen trailers within the pool; reset the cycle when all have been shown.
|
||||||
pool = ApplyAvoidRepeats(pool, user.Id, config.AvoidRepeats);
|
pool = ApplyAvoidRepeats(pool, user.Id, config.AvoidRepeats);
|
||||||
@@ -229,6 +236,12 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TMDB tags anime as "Animation"; anime metadata providers (e.g. AniDB) use "Anime".
|
||||||
|
private static readonly string[] AnimatedGenres = { "Animation", "Anime" };
|
||||||
|
|
||||||
|
private static bool IsAnimated(BaseItem item)
|
||||||
|
=> item.Genres.Any(g => AnimatedGenres.Contains(g, StringComparer.OrdinalIgnoreCase));
|
||||||
|
|
||||||
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
||||||
List<(Movie Movie, BaseItem Trailer)> pairs,
|
List<(Movie Movie, BaseItem Trailer)> pairs,
|
||||||
BaseItem feature,
|
BaseItem feature,
|
||||||
|
|||||||
11
build.yaml
11
build.yaml
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: 1.0.0.9
|
version: 1.0.0.11
|
||||||
name: CinemaTrailers4Jellyfins
|
name: CinemaTrailers4Jellyfins
|
||||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||||
description: >
|
description: >
|
||||||
@@ -12,10 +12,11 @@ category: General
|
|||||||
owner: 514mart
|
owner: 514mart
|
||||||
targetAbi: 10.11.0.0
|
targetAbi: 10.11.0.0
|
||||||
changelog:
|
changelog:
|
||||||
- Skip trailers and pre-roll/feature pre-roll bumpers the current user
|
- Animated trailers (anime included) now only play before animated
|
||||||
can't actually access (e.g. a library not in their library access list)
|
content. Before, an anime trailer could play before a live-action
|
||||||
— some Cinema Mode clients hang indefinitely trying to play an
|
movie or show when they shared a genre like Drama or Action, or when
|
||||||
inaccessible item, so these are now filtered out per-user instead
|
no trailer matched the genre filter. A live-action feature now gets no
|
||||||
|
trailer rather than an animated one
|
||||||
|
|
||||||
dotnetProjects:
|
dotnetProjects:
|
||||||
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||||
|
|||||||
@@ -8,6 +8,22 @@
|
|||||||
"owner": "514mart",
|
"owner": "514mart",
|
||||||
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
|
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"checksum": "13e6467ce568b1abe987ef5a8a0ee95d",
|
||||||
|
"changelog": "- Fix trailer downloads, broken by a YouTube change. YouTube no longer serves combined video+audio files, so the built-in downloader now fetches the video and audio separately, merges them with ffmpeg (no re-encode) and deletes the separate files afterwards\n- A failed download no longer leaves an empty trailer folder behind. Empty folders counted toward \"Max trailers to keep\" (pushing out real trailers) and were never retried\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.10/cinematrailers4jellyfins_1.0.0.10.zip",
|
||||||
|
"timestamp": "2026-10-05T21:42:55Z",
|
||||||
|
"version": "1.0.0.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksum": "9594d14a5fed94ee5eea55d72c71728c",
|
||||||
|
"changelog": "- Skip trailers and pre-roll/feature pre-roll bumpers the current user can't actually access (e.g. a library not in their library access list) \u2014 some Cinema Mode clients hang indefinitely trying to play an inaccessible item, so these are now filtered out per-user instead\n",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.9/cinematrailers4jellyfins_1.0.0.9.zip",
|
||||||
|
"timestamp": "2026-06-10T17:07:24Z",
|
||||||
|
"version": "1.0.0.9"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksum": "89c66caa789da56ead7d60e01d3207a5",
|
"checksum": "89c66caa789da56ead7d60e01d3207a5",
|
||||||
"changelog": "- Fix Trailer Pre-Roll / Feature Pre-Roll bumpers not being found \u2014 the library lookup now walks the configured library folder's children directly instead of relying on TopParentIds matching, which could fail to match movies even when they appear correctly in the library\n",
|
"changelog": "- Fix Trailer Pre-Roll / Feature Pre-Roll bumpers not being found \u2014 the library lookup now walks the configured library folder's children directly instead of relying on TopParentIds matching, which could fail to match movies even when they appear correctly in the library\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user