10 Commits

Author SHA1 Message Date
Martin
5176582391 fix: download trailers as separate video+audio and merge with ffmpeg (v1.0.0.10)
All checks were successful
Publish Release / release (push) Successful in 26s
YouTube stopped serving muxed streams and YoutubeExplode 6.6.0 failed on
every video ("Video is not available"), so no trailer downloaded. Bump
YoutubeExplode to 6.6.2, download the best video-only + audio-only streams
and stream-copy them into one MP4 with ffmpeg. The separate files are
always deleted afterwards, merge or not.

Also delete the trailer folder when a download fails. Empty folders were
treated as "already downloaded" (never retried) and counted toward
MaxTotalTrailers, so over time they pushed out every real trailer. The
skip check now requires the trailer file itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:42:28 -04:00
gitea-actions
e518b79c36 chore: update manifest.json for v1.0.0.9 2026-06-10 17:07:24 +00:00
Martin
6bb23dc9c4 feat: skip trailers/pre-rolls inaccessible to the current user (v1.0.0.9)
All checks were successful
Publish Release / release (push) Successful in 12s
Filters trailer pool items and pre-roll/feature pre-roll bumpers using
BaseItem.IsVisibleStandalone(user), so users without access to the
relevant library are never handed an item they can't actually play —
some Cinema Mode clients hang indefinitely in that case.
2026-06-10 13:07:11 -04:00
gitea-actions
5f7a76feb5 chore: update manifest.json for v1.0.0.8 2026-06-10 15:28:02 +00:00
Martin
adfa570569 fix: pre-roll/feature pre-roll bumpers not found in library (v1.0.0.8)
All checks were successful
Publish Release / release (push) Successful in 8s
TopParentIds-based queries could fail to match Movie items in the
configured pre-roll library even when they appear correctly in the
Jellyfin UI. Look up the library Folder by ID and walk its recursive
children directly instead, matching the path-based approach already
used for the trailer pool.
2026-06-10 11:27:52 -04:00
gitea-actions
99798b5174 chore: update manifest.json for v1.0.0.7 2026-06-10 15:10:56 +00:00
Martin
c76ddbc352 chore: trace GetIntros calls and outcomes at Information level (v1.0.0.7)
All checks were successful
Publish Release / release (push) Successful in 10s
Logs every GetIntros call (item, path, enabled features), why an item
is skipped, and pre-roll/post-roll lookup outcomes, without depending
on a Debug logging.json override.
2026-06-10 11:09:48 -04:00
gitea-actions
e9d9543941 chore: update manifest.json for v1.0.0.6 2026-06-10 14:35:52 +00:00
Martin
ba56926dc2 chore: add diagnostic logging for pre-roll bumpers (v1.0.0.6)
All checks were successful
Publish Release / release (push) Successful in 13s
Helps diagnose why a Trailer Pre-Roll/Feature Pre-Roll bumper isn't
playing by logging invalid library IDs, empty library results, and
the movie picked.
2026-06-10 10:35:33 -04:00
gitea-actions
2ef39853eb chore: update manifest.json for v1.0.0.5 2026-06-10 05:14:23 +00:00
8 changed files with 246 additions and 42 deletions

View File

@@ -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.5</AssemblyVersion> <AssemblyVersion>1.0.0.10</AssemblyVersion>
<FileVersion>1.0.0.5</FileVersion> <FileVersion>1.0.0.10</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>

View File

@@ -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)

View File

@@ -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,

View File

@@ -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";
}
}

View File

@@ -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(

View File

@@ -48,6 +48,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
var preRollEnabled = !string.IsNullOrEmpty(config.TrailerPreRollLibraryId); var preRollEnabled = !string.IsNullOrEmpty(config.TrailerPreRollLibraryId);
var featurePreRollEnabled = !string.IsNullOrEmpty(config.FeaturePreRollLibraryId); var featurePreRollEnabled = !string.IsNullOrEmpty(config.FeaturePreRollLibraryId);
_logger.LogInformation(
"|CinemaTrailers4Jellyfins| GetIntros called for {Type} '{Name}' (Path={Path}). "
+ "trailersEnabled={Trailers} preRollEnabled={PreRoll} featurePreRollEnabled={FeaturePreRoll}",
item.GetType().Name,
item.Name,
item.Path,
trailersEnabled,
preRollEnabled,
featurePreRollEnabled);
if (!trailersEnabled && !preRollEnabled && !featurePreRollEnabled) if (!trailersEnabled && !preRollEnabled && !featurePreRollEnabled)
return Task.FromResult(Enumerable.Empty<IntroInfo>()); return Task.FromResult(Enumerable.Empty<IntroInfo>());
@@ -65,16 +75,23 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
case Episode episode: case Episode episode:
if (!config.TrailersForEpisodes) if (!config.TrailersForEpisodes)
{
_logger.LogInformation("|CinemaTrailers4Jellyfins| Skipping: TrailersForEpisodes is disabled.");
return Task.FromResult(Enumerable.Empty<IntroInfo>()); return Task.FromResult(Enumerable.Empty<IntroInfo>());
}
if (!IsFirstEpisodeOfDay(user.Id)) if (!IsFirstEpisodeOfDay(user.Id))
{
_logger.LogInformation("|CinemaTrailers4Jellyfins| Skipping: not the first episode of the day for this user.");
return Task.FromResult(Enumerable.Empty<IntroInfo>()); return Task.FromResult(Enumerable.Empty<IntroInfo>());
}
feature = episode.Series ?? (BaseItem)episode; feature = episode.Series ?? (BaseItem)episode;
isEpisode = true; isEpisode = true;
break; break;
default: default:
_logger.LogInformation("|CinemaTrailers4Jellyfins| Skipping: item type {Type} is not a Movie or Episode.", item.GetType().Name);
return Task.FromResult(Enumerable.Empty<IntroInfo>()); return Task.FromResult(Enumerable.Empty<IntroInfo>());
} }
@@ -85,13 +102,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
// (covers both the fake-movie files and their trailer extras). // (covers both the fake-movie files and their trailer extras).
if (!string.IsNullOrEmpty(outputFolder) if (!string.IsNullOrEmpty(outputFolder)
&& item.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true) && item.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true)
{
_logger.LogInformation("|CinemaTrailers4Jellyfins| Skipping: item path is inside the output folder ({Folder}).", outputFolder);
return Task.FromResult(Enumerable.Empty<IntroInfo>()); return Task.FromResult(Enumerable.Empty<IntroInfo>());
}
var intros = new List<IntroInfo>(); var intros = new List<IntroInfo>();
if (preRollEnabled) if (preRollEnabled)
{ {
var preRoll = GetRandomLibraryMovieIntro(config.TrailerPreRollLibraryId, item.Id); var preRoll = GetRandomLibraryMovieIntro("Trailer Pre-Roll", config.TrailerPreRollLibraryId, item.Id, user);
if (preRoll != null) if (preRoll != null)
intros.Add(preRoll); intros.Add(preRoll);
} }
@@ -109,6 +129,9 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
.Where(m => .Where(m =>
m.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true m.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true
&& m.LocalTrailers.Count > 0 && m.LocalTrailers.Count > 0
// Skip items the user can't actually play (e.g. library not in their access list) —
// some clients hang indefinitely trying to play an inaccessible item.
&& m.IsVisibleStandalone(user)
// Keep TV show trailers for episodes and movie trailers for movies separate. // Keep TV show trailers for episodes and movie trailers for movies separate.
&& m.Tags.Contains(TrailerTags.TvShow, StringComparer.OrdinalIgnoreCase) == isEpisode) && m.Tags.Contains(TrailerTags.TvShow, StringComparer.OrdinalIgnoreCase) == isEpisode)
.SelectMany(m => m.LocalTrailers.Select(t => (Movie: m, Trailer: t))) .SelectMany(m => m.LocalTrailers.Select(t => (Movie: m, Trailer: t)))
@@ -145,11 +168,16 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
if (featurePreRollEnabled) if (featurePreRollEnabled)
{ {
var featureRoll = GetRandomLibraryMovieIntro(config.FeaturePreRollLibraryId, item.Id); var featureRoll = GetRandomLibraryMovieIntro("Feature Pre-Roll", config.FeaturePreRollLibraryId, item.Id, user);
if (featureRoll != null) if (featureRoll != null)
intros.Add(featureRoll); intros.Add(featureRoll);
} }
_logger.LogInformation(
"|CinemaTrailers4Jellyfins| Returning {Count} intro(s): {Paths}",
intros.Count,
string.Join(", ", intros.Select(i => i.Path)));
return Task.FromResult<IEnumerable<IntroInfo>>(intros); return Task.FromResult<IEnumerable<IntroInfo>>(intros);
} }
@@ -157,26 +185,47 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
/// Picks a random Movie from the given Jellyfin library (VirtualFolder ItemId) to use as a /// Picks a random Movie from the given Jellyfin library (VirtualFolder ItemId) to use as a
/// pre-roll/post-roll bumper, excluding the item currently being played. /// pre-roll/post-roll bumper, excluding the item currently being played.
/// </summary> /// </summary>
private IntroInfo? GetRandomLibraryMovieIntro(string libraryId, Guid excludeId) private IntroInfo? GetRandomLibraryMovieIntro(string label, string libraryId, Guid excludeId, User user)
{ {
if (!Guid.TryParse(libraryId, out var parsedId)) if (!Guid.TryParse(libraryId, out var parsedId))
return null;
var movies = _libraryManager
.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { BaseItemKind.Movie }, _logger.LogWarning(
TopParentIds = new[] { parsedId }, "|CinemaTrailers4Jellyfins| {Label} library ID '{LibraryId}' is not a valid GUID — check the plugin settings.",
Recursive = true, label,
}) libraryId);
return null;
}
if (_libraryManager.GetItemById(parsedId) is not Folder folder)
{
_logger.LogWarning(
"|CinemaTrailers4Jellyfins| {Label} library {LibraryId} could not be found.",
label,
parsedId);
return null;
}
var movies = folder.GetRecursiveChildren()
.OfType<Movie>() .OfType<Movie>()
.Where(m => m.Id != excludeId) .Where(m => m.Id != excludeId && m.IsVisibleStandalone(user))
.ToList(); .ToList();
if (movies.Count == 0) if (movies.Count == 0)
{
_logger.LogInformation(
"|CinemaTrailers4Jellyfins| {Label} library {LibraryId} has no eligible Movie items visible to this user. "
+ "Ensure the library has been scanned, contains at least one other Movie, and is in this user's library access list.",
label,
parsedId);
return null; return null;
}
var movie = movies[_rng.Next(movies.Count)]; var movie = movies[_rng.Next(movies.Count)];
_logger.LogInformation(
"|CinemaTrailers4Jellyfins| {Label}: picked '{Title}' ({Path}).",
label,
movie.Name,
movie.Path);
return new IntroInfo { ItemId = movie.Id, Path = movie.Path }; return new IntroInfo { ItemId = movie.Id, Path = movie.Path };
} }

View File

@@ -1,5 +1,5 @@
--- ---
version: 1.0.0.5 version: 1.0.0.10
name: CinemaTrailers4Jellyfins name: CinemaTrailers4Jellyfins
guid: b581493e-1046-40ed-b6dc-cb8027624984 guid: b581493e-1046-40ed-b6dc-cb8027624984
description: > description: >
@@ -12,10 +12,13 @@ category: General
owner: 514mart owner: 514mart
targetAbi: 10.11.0.0 targetAbi: 10.11.0.0
changelog: changelog:
- Add Trailer Pre-Roll and Feature Pre-Roll bumpers via IIntroProvider — pick - Fix trailer downloads, broken by a YouTube change. YouTube no longer
existing Jellyfin Movie libraries in settings and a random movie from each is serves combined video+audio files, so the built-in downloader now fetches
played before the trailer block ("Now Playing") and/or right before the the video and audio separately, merges them with ffmpeg (no re-encode)
feature ("Feature Presentation"); both are independent and disabled by default and deletes the separate files afterwards
- 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
dotnetProjects: dotnetProjects:
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins - name: Jellyfin.Plugin.CinemaTrailers4Jellyfins

View File

@@ -8,6 +8,46 @@
"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": "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",
"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",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.8/cinematrailers4jellyfins_1.0.0.8.zip",
"timestamp": "2026-06-10T15:28:02Z",
"version": "1.0.0.8"
},
{
"checksum": "a52f1354c9737cf9ec92f588bb4e750e",
"changelog": "- Add Information-level diagnostic logging to IIntroProvider.GetIntros \u2014 logs every call (item, path, and which features are enabled), why an item is skipped, and the outcome of Trailer/Feature Pre-Roll lookups, to help troubleshoot why a pre-roll bumper isn't playing\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.7/cinematrailers4jellyfins_1.0.0.7.zip",
"timestamp": "2026-06-10T15:10:56Z",
"version": "1.0.0.7"
},
{
"checksum": "96e0624e5173e5e6bf829d47118e1b40",
"changelog": "- Add diagnostic logging for Trailer Pre-Roll and Feature Pre-Roll bumpers \u2014 logs a warning if the configured library ID is invalid, and a debug message when no eligible movie is found or which movie was picked, to help troubleshoot why a bumper isn't playing\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.6/cinematrailers4jellyfins_1.0.0.6.zip",
"timestamp": "2026-06-10T14:35:52Z",
"version": "1.0.0.6"
},
{
"checksum": "6c88aaeb38023b327e9b042181dc0708",
"changelog": "- Add Trailer Pre-Roll and Feature Pre-Roll bumpers via IIntroProvider \u2014 pick existing Jellyfin Movie libraries in settings and a random movie from each is played before the trailer block (\"Now Playing\") and/or right before the feature (\"Feature Presentation\"); both are independent and disabled by default\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.5/cinematrailers4jellyfins_1.0.0.5.zip",
"timestamp": "2026-06-10T05:14:23Z",
"version": "1.0.0.5"
},
{ {
"checksum": "af6051b002939e4e5cd6ef4baf5c85d4", "checksum": "af6051b002939e4e5cd6ef4baf5c85d4",
"changelog": "- Add TV show trailer downloads \u2014 separate TMDB sources (Airing Today, On The Air, Popular, Top Rated) and a \"Max TV show trailers per run\" cap independent from movies; set either cap to 0 to skip that category entirely\n- Add trailers before TV episodes via IIntroProvider \u2014 only the first episode a user watches each day gets trailers, picked using the show's genre/rating for filtering\n- Movie and TV trailers are now kept separate \u2014 movies only ever get movie trailers and episodes only ever get TV show trailers, via a tag written to each trailer's NFO at download time\n", "changelog": "- Add TV show trailer downloads \u2014 separate TMDB sources (Airing Today, On The Air, Popular, Top Rated) and a \"Max TV show trailers per run\" cap independent from movies; set either cap to 0 to skip that category entirely\n- Add trailers before TV episodes via IIntroProvider \u2014 only the first episode a user watches each day gets trailers, picked using the show's genre/rating for filtering\n- Movie and TV trailers are now kept separate \u2014 movies only ever get movie trailers and episodes only ever get TV show trailers, via a tag written to each trailer's NFO at download time\n",