Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e518b79c36 | ||
|
|
6bb23dc9c4 | ||
|
|
5f7a76feb5 | ||
|
|
adfa570569 | ||
|
|
99798b5174 | ||
|
|
c76ddbc352 | ||
|
|
e9d9543941 | ||
|
|
ba56926dc2 | ||
|
|
2ef39853eb | ||
|
|
a0bddac48d | ||
|
|
18c3c49a26 |
@@ -68,5 +68,15 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration
|
|||||||
|
|
||||||
/// <summary>Also inject trailers before TV episodes, but only before the first episode a user watches each day.</summary>
|
/// <summary>Also inject trailers before TV episodes, but only before the first episode a user watches each day.</summary>
|
||||||
public bool TrailersForEpisodes { get; set; } = false;
|
public bool TrailersForEpisodes { get; set; } = false;
|
||||||
|
|
||||||
|
// ── Pre-Roll Bumpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Jellyfin movie library (VirtualFolder ItemId) to pick a random "Trailer Pre-Roll"
|
||||||
|
/// bumper from, played before the trailer block. Empty = disabled.</summary>
|
||||||
|
public string TrailerPreRollLibraryId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Jellyfin movie library (VirtualFolder ItemId) to pick a random "Feature Pre-Roll"
|
||||||
|
/// bumper from, played after the trailer block, right before the feature. Empty = disabled.</summary>
|
||||||
|
public string FeaturePreRollLibraryId { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,6 +341,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Pre-Roll Bumpers -->
|
||||||
|
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||||
|
<legend><h3 class="sectionTitle">Pre-Roll Bumpers</h3></legend>
|
||||||
|
<p class="fieldDescription">
|
||||||
|
Optional: pick existing Jellyfin Movie libraries to pull random bumper
|
||||||
|
videos from, bookending the trailer block above. Each is independent —
|
||||||
|
leave either set to "None" to disable it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="selectContainer">
|
||||||
|
<label class="selectLabel" for="trailer-preroll-library">Trailer Pre-Roll library</label>
|
||||||
|
<select is="emby-select" id="trailer-preroll-library" class="emby-select-withcolor emby-select">
|
||||||
|
<option value="">— None (disabled) —</option>
|
||||||
|
</select>
|
||||||
|
<div class="fieldDescription">
|
||||||
|
A Movie library to pick a random "Now Playing" style bumper from,
|
||||||
|
played before the trailer block.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selectContainer">
|
||||||
|
<label class="selectLabel" for="feature-preroll-library">Feature Pre-Roll library</label>
|
||||||
|
<select is="emby-select" id="feature-preroll-library" class="emby-select-withcolor emby-select">
|
||||||
|
<option value="">— None (disabled) —</option>
|
||||||
|
</select>
|
||||||
|
<div class="fieldDescription">
|
||||||
|
A Movie library to pick a random "Feature Presentation" style bumper
|
||||||
|
from, played right before the movie/episode (after trailers).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||||
<legend><h3 class="sectionTitle">Advanced</h3></legend>
|
<legend><h3 class="sectionTitle">Advanced</h3></legend>
|
||||||
@@ -375,7 +407,24 @@
|
|||||||
|
|
||||||
$('.cinemaTrailers4JellyfinsConfigPage').on('pageshow', function () {
|
$('.cinemaTrailers4JellyfinsConfigPage').on('pageshow', function () {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
|
Promise.all([
|
||||||
|
ApiClient.getPluginConfiguration(pluginId),
|
||||||
|
ApiClient.getJSON(ApiClient.getUrl('Library/VirtualFolders'))
|
||||||
|
]).then(function (results) {
|
||||||
|
var config = results[0];
|
||||||
|
var movieFolders = results[1].filter(function (f) { return f.CollectionType === 'movies'; });
|
||||||
|
[['trailer-preroll-library', config.TrailerPreRollLibraryId],
|
||||||
|
['feature-preroll-library', config.FeaturePreRollLibraryId]]
|
||||||
|
.forEach(function (entry) {
|
||||||
|
var select = document.getElementById(entry[0]);
|
||||||
|
movieFolders.forEach(function (f) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = f.ItemId;
|
||||||
|
opt.text = f.Name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
select.value = entry[1] || '';
|
||||||
|
});
|
||||||
document.getElementById('tmdb-api-key').value = config.TmdbApiKey || '';
|
document.getElementById('tmdb-api-key').value = config.TmdbApiKey || '';
|
||||||
document.getElementById('source-now-playing').checked = config.SourceNowPlaying !== false;
|
document.getElementById('source-now-playing').checked = config.SourceNowPlaying !== false;
|
||||||
document.getElementById('source-upcoming').checked = config.SourceUpcoming !== false;
|
document.getElementById('source-upcoming').checked = config.SourceUpcoming !== false;
|
||||||
@@ -442,6 +491,8 @@
|
|||||||
config.FilterByRating = document.getElementById('filter-rating').checked;
|
config.FilterByRating = document.getElementById('filter-rating').checked;
|
||||||
config.AvoidRepeats = document.getElementById('avoid-repeats').checked;
|
config.AvoidRepeats = document.getElementById('avoid-repeats').checked;
|
||||||
config.TrailersForEpisodes = document.getElementById('trailers-for-episodes').checked;
|
config.TrailersForEpisodes = document.getElementById('trailers-for-episodes').checked;
|
||||||
|
config.TrailerPreRollLibraryId = document.getElementById('trailer-preroll-library').value;
|
||||||
|
config.FeaturePreRollLibraryId = document.getElementById('feature-preroll-library').value;
|
||||||
ApiClient.updatePluginConfiguration(pluginId, config)
|
ApiClient.updatePluginConfiguration(pluginId, config)
|
||||||
.then(Dashboard.processPluginConfigurationUpdateResult);
|
.then(Dashboard.processPluginConfigurationUpdateResult);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.4</AssemblyVersion>
|
<AssemblyVersion>1.0.0.9</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.4</FileVersion>
|
<FileVersion>1.0.0.9</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
|||||||
@@ -41,7 +41,24 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
public Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user)
|
public Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user)
|
||||||
{
|
{
|
||||||
var config = Plugin.Instance?.Configuration;
|
var config = Plugin.Instance?.Configuration;
|
||||||
if (config == null || config.TrailersPerMovie <= 0)
|
if (config == null)
|
||||||
|
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||||
|
|
||||||
|
var trailersEnabled = config.TrailersPerMovie > 0;
|
||||||
|
var preRollEnabled = !string.IsNullOrEmpty(config.TrailerPreRollLibraryId);
|
||||||
|
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)
|
||||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||||
|
|
||||||
// The item used for genre/rating filtering. For episodes this is the
|
// The item used for genre/rating filtering. For episodes this is the
|
||||||
@@ -58,29 +75,49 @@ 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>());
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputFolder = config.DownloadFolder?.TrimEnd(
|
var outputFolder = config.DownloadFolder?.TrimEnd(
|
||||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
if (string.IsNullOrEmpty(outputFolder))
|
|
||||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
|
||||||
|
|
||||||
// Break recursion: don't inject intros before items from our own output folder
|
// Break recursion: don't inject intros before items from our own output folder
|
||||||
// (covers both the fake-movie files and their trailer extras).
|
// (covers both the fake-movie files and their trailer extras).
|
||||||
if (item.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true)
|
if (!string.IsNullOrEmpty(outputFolder)
|
||||||
|
&& 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>();
|
||||||
|
|
||||||
|
if (preRollEnabled)
|
||||||
|
{
|
||||||
|
var preRoll = GetRandomLibraryMovieIntro("Trailer Pre-Roll", config.TrailerPreRollLibraryId, item.Id, user);
|
||||||
|
if (preRoll != null)
|
||||||
|
intros.Add(preRoll);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trailersEnabled && !string.IsNullOrEmpty(outputFolder))
|
||||||
|
{
|
||||||
// Server-scoped query so hidden libraries don't block trailer discovery.
|
// Server-scoped query so hidden libraries don't block trailer discovery.
|
||||||
var pairs = _libraryManager
|
var pairs = _libraryManager
|
||||||
.GetItemList(new InternalItemsQuery
|
.GetItemList(new InternalItemsQuery
|
||||||
@@ -92,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)))
|
||||||
@@ -104,9 +144,9 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
+ "Ensure the output folder is added as a Jellyfin Movies library and scanned.",
|
+ "Ensure the output folder is added as a Jellyfin Movies library and scanned.",
|
||||||
isEpisode ? "TV show" : "movie",
|
isEpisode ? "TV show" : "movie",
|
||||||
outputFolder);
|
outputFolder);
|
||||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Apply enabled filters. If nothing survives, fall back to the full pool.
|
// Apply enabled filters. If nothing survives, fall back to the full pool.
|
||||||
var filtered = ApplyFilters(pairs, feature, config);
|
var filtered = ApplyFilters(pairs, feature, config);
|
||||||
var pool = filtered.Count > 0 ? filtered : pairs;
|
var pool = filtered.Count > 0 ? filtered : pairs;
|
||||||
@@ -122,8 +162,71 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
|||||||
if (config.AvoidRepeats)
|
if (config.AvoidRepeats)
|
||||||
MarkSeen(user.Id, selected.Select(p => p.Trailer.Id));
|
MarkSeen(user.Id, selected.Select(p => p.Trailer.Id));
|
||||||
|
|
||||||
var intros = selected.Select(p => new IntroInfo { ItemId = p.Trailer.Id, Path = p.Trailer.Path });
|
intros.AddRange(selected.Select(p => new IntroInfo { ItemId = p.Trailer.Id, Path = p.Trailer.Path }));
|
||||||
return Task.FromResult<IEnumerable<IntroInfo>>(intros.ToList());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featurePreRollEnabled)
|
||||||
|
{
|
||||||
|
var featureRoll = GetRandomLibraryMovieIntro("Feature Pre-Roll", config.FeaturePreRollLibraryId, item.Id, user);
|
||||||
|
if (featureRoll != null)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private IntroInfo? GetRandomLibraryMovieIntro(string label, string libraryId, Guid excludeId, User user)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(libraryId, out var parsedId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"|CinemaTrailers4Jellyfins| {Label} library ID '{LibraryId}' is not a valid GUID — check the plugin settings.",
|
||||||
|
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>()
|
||||||
|
.Where(m => m.Id != excludeId && m.IsVisibleStandalone(user))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
private static List<(Movie Movie, BaseItem Trailer)> ApplyFilters(
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -1,30 +1,70 @@
|
|||||||
# CinemaTrailers4Jellyfins
|
# CinemaTrailers4Jellyfins
|
||||||
|
|
||||||
A Jellyfin plugin that automatically downloads movie trailers from TMDB/YouTube and packages
|
A Jellyfin plugin that automatically downloads movie and TV show trailers from TMDB/YouTube and
|
||||||
each one as a self-contained "fake movie" folder, ready to be picked up by a Cinema Mode /
|
packages each one as a self-contained "fake movie" folder, ready to be picked up by a Cinema
|
||||||
trailer pre-roll plugin.
|
Mode / trailer pre-roll plugin. It can also act as a Cinema Mode pre-roll source itself, via
|
||||||
|
Jellyfin's `IIntroProvider` interface (compatible with Wholphin and similar clients).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. A daily scheduled task fetches candidate movies from TMDB (Now Playing, Upcoming, Popular,
|
### Downloading trailers
|
||||||
Top Rated — configurable), optionally skipping movies already in your library.
|
|
||||||
|
1. A daily scheduled task fetches candidate movies and/or TV shows from TMDB (Now Playing,
|
||||||
|
Upcoming, Popular, Top Rated for movies; Airing Today, On The Air, Popular, Top Rated for TV
|
||||||
|
shows — all configurable), optionally skipping titles already in your library.
|
||||||
2. For each candidate, it fetches the official trailer(s) from TMDB, which point to YouTube.
|
2. For each candidate, it fetches the official trailer(s) from TMDB, which point to YouTube.
|
||||||
3. It downloads the trailer video and builds a folder for it:
|
3. It downloads the trailer video and builds a folder for it:
|
||||||
```
|
```
|
||||||
{OutputFolder}/
|
{OutputFolder}/
|
||||||
{Movie Title} ({Year})/
|
{Title} ({Year})/
|
||||||
{Movie Title} ({Year}).mp4 ← placeholder "fake movie" (copy of a master file)
|
{Title} ({Year}).mp4 ← placeholder "fake movie" (copy of a master file)
|
||||||
{Movie Title} ({Year})-trailer.mp4 ← the actual downloaded trailer
|
{Title} ({Year})-trailer.mp4 ← the actual downloaded trailer
|
||||||
{Movie Title} ({Year}).nfo ← minimal NFO (title, year, locked metadata)
|
{Title} ({Year}).nfo ← minimal NFO (title, year, genres, rating, locked metadata)
|
||||||
```
|
```
|
||||||
|
TV show trailers are tagged (`<tag>ct4j-tvshow</tag>`) in their NFO so they can be told apart
|
||||||
|
from movie trailers.
|
||||||
4. Jellyfin scans the placeholder file as a normal movie (its metadata locked via the NFO so
|
4. Jellyfin scans the placeholder file as a normal movie (its metadata locked via the NFO so
|
||||||
it never queries TMDB for it) and picks up the adjacent `-trailer.mp4` as that movie's
|
it never queries TMDB for it) and picks up the adjacent `-trailer.mp4` as that item's
|
||||||
local trailer — which a Cinema Mode / trailer pre-roll plugin can then play.
|
local trailer — which a Cinema Mode / trailer pre-roll plugin can then play.
|
||||||
|
|
||||||
The placeholder "fake movie" is a few seconds of black video with silent audio — just enough
|
The placeholder "fake movie" is a few seconds of black video with silent audio — just enough
|
||||||
for Jellyfin to treat the file as a valid video. It's generated once via `ffmpeg` and reused
|
for Jellyfin to treat the file as a valid video. It's generated once via `ffmpeg` and reused
|
||||||
by copying, not regenerated for every trailer.
|
by copying, not regenerated for every trailer.
|
||||||
|
|
||||||
|
Movie and TV show downloads each have their own TMDB sources and their own "max trailers per
|
||||||
|
run" setting — set either to 0 to skip that category entirely.
|
||||||
|
|
||||||
|
### Playing trailers (Cinema Mode integration)
|
||||||
|
|
||||||
|
The plugin also registers as an `IIntroProvider`, so Jellyfin clients with cinema mode support
|
||||||
|
(and clients like Wholphin) can play the downloaded trailers as pre-rolls directly, without a
|
||||||
|
separate trailer plugin:
|
||||||
|
|
||||||
|
- Before a **movie**, it injects movie trailers from the output folder.
|
||||||
|
- Before a **TV episode** (if enabled), it injects TV show trailers — but only before the first
|
||||||
|
episode a user watches each day. If you binge several episodes in one sitting, only the first
|
||||||
|
one gets trailers; the next day it resets.
|
||||||
|
- Movie and TV trailers are kept in separate pools, so movies only ever get movie trailers and
|
||||||
|
episodes only ever get TV show trailers.
|
||||||
|
- Optional filters can restrict trailers to those matching the genre and/or age rating of the
|
||||||
|
movie/show you're about to watch (falling back to the full pool if nothing matches), and an
|
||||||
|
"avoid repeats" mode cycles through all trailers before repeating any.
|
||||||
|
|
||||||
|
The output folder still needs to be added as a Jellyfin **Movies** library and scanned for any
|
||||||
|
of this to work, since that's how Jellyfin discovers the trailer files in the first place.
|
||||||
|
|
||||||
|
#### Pre-roll bumpers
|
||||||
|
|
||||||
|
Optionally, you can also bookend the trailer block with bumper videos picked at random from
|
||||||
|
your own existing Jellyfin **Movies** libraries:
|
||||||
|
|
||||||
|
- **Trailer Pre-Roll**: plays *before* the trailer block (e.g. a "Now Playing" bumper).
|
||||||
|
- **Feature Pre-Roll**: plays *after* the trailer block, right before the movie/episode itself
|
||||||
|
(e.g. a "Feature Presentation" bumper).
|
||||||
|
|
||||||
|
Each is independent, disabled by default, and picks a plain random `Movie` item from the
|
||||||
|
configured library every time (no genre/rating filtering or repeat-avoidance).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Jellyfin 10.11+
|
- Jellyfin 10.11+
|
||||||
@@ -58,15 +98,24 @@ Go to **Admin → Plugins → CinemaTrailers4Jellyfins**.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **TMDB API Key** | Your TMDB Read Access Token (JWT) or v3 API key |
|
| **TMDB API Key** | Your TMDB Read Access Token (JWT) or v3 API key |
|
||||||
| **Trailer Languages** | Restrict downloads to specific trailer languages |
|
| **Trailer Languages** | Restrict downloads to specific trailer languages |
|
||||||
| **Trailer Sources** | Which TMDB lists to pull candidates from (Now Playing, Upcoming, Popular, Top Rated) |
|
| **Movie Trailer Sources** | Which TMDB lists to pull movie candidates from (Now Playing, Upcoming, Popular, Top Rated) |
|
||||||
| **Date Range** | Only consider movies released within the last N months |
|
| **TV Show Trailer Sources** | Which TMDB lists to pull TV show candidates from (Airing Today, On The Air, Popular, Top Rated) |
|
||||||
|
| **Date Range** | Only consider titles released (or first-aired) within the last N months |
|
||||||
| **Output Folder** | Where the fake-movie folders are created |
|
| **Output Folder** | Where the fake-movie folders are created |
|
||||||
| **Max trailers per run** | How many trailers to download per task run |
|
| **Max movie trailers per run** | How many movie trailers to download per task run. 0 = don't download movie trailers |
|
||||||
|
| **Max TV show trailers per run** | How many TV show trailers to download per task run. 0 = don't download TV show trailers |
|
||||||
| **Pages per source** | How many TMDB pages to fetch per source |
|
| **Pages per source** | How many TMDB pages to fetch per source |
|
||||||
| **Video quality** | 720p / 480p (built-in) or 1080p (requires yt-dlp) |
|
| **Video quality** | 720p / 480p (built-in) or 1080p (requires yt-dlp) |
|
||||||
| **Skip movies already in my Jellyfin library** | Don't download trailers for movies you already own |
|
| **Skip movies/shows already in my Jellyfin library** | Don't download trailers for movies/shows you already own |
|
||||||
| **Skip trailers already downloaded** | Don't re-download a trailer if its folder already exists |
|
| **Skip trailers already downloaded** | Don't re-download a trailer if its folder already exists |
|
||||||
| **Max trailers to keep** | Oldest trailer folders are deleted first once this cap is exceeded |
|
| **Max trailers to keep** | Oldest trailer folders are deleted first once this cap is exceeded |
|
||||||
|
| **Trailers per movie** | How many trailers to play before each item via Cinema Mode (`IIntroProvider`). 0 = disabled |
|
||||||
|
| **Match genre to the movie being played** | Only pick trailers whose genre overlaps with what you're about to watch |
|
||||||
|
| **Limit to same age rating or lower** | Never play a trailer rated higher than what you're about to watch |
|
||||||
|
| **Avoid repeating trailers** | Cycle through all available trailers before repeating any |
|
||||||
|
| **Also play trailers before TV episodes** | Plays before only the first episode a user watches each day |
|
||||||
|
| **Trailer Pre-Roll library** | Movie library to pick a random "Now Playing" style bumper from, played before the trailer block. None = disabled |
|
||||||
|
| **Feature Pre-Roll library** | Movie library to pick a random "Feature Presentation" style bumper from, played after the trailer block. None = disabled |
|
||||||
| **yt-dlp path** | Optional path to `yt-dlp` for 1080p+ downloads |
|
| **yt-dlp path** | Optional path to `yt-dlp` for 1080p+ downloads |
|
||||||
|
|
||||||
## Running the task
|
## Running the task
|
||||||
@@ -75,7 +124,8 @@ After configuring, go to **Admin → Scheduled Tasks → CinemaTrailers4Jellyfin
|
|||||||
to do an immediate download pass. The task then runs automatically once per day.
|
to do an immediate download pass. The task then runs automatically once per day.
|
||||||
|
|
||||||
After the task completes, add the output folder as a Jellyfin **Movies** library (and run a
|
After the task completes, add the output folder as a Jellyfin **Movies** library (and run a
|
||||||
library scan) so your Cinema Mode / trailer pre-roll plugin can use the trailers.
|
library scan) so your Cinema Mode / trailer pre-roll plugin (or this plugin's own
|
||||||
|
`IIntroProvider`) can use the trailers.
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
|
|||||||
15
build.yaml
15
build.yaml
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: 1.0.0.4
|
version: 1.0.0.9
|
||||||
name: CinemaTrailers4Jellyfins
|
name: CinemaTrailers4Jellyfins
|
||||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||||
description: >
|
description: >
|
||||||
@@ -12,15 +12,10 @@ category: General
|
|||||||
owner: 514mart
|
owner: 514mart
|
||||||
targetAbi: 10.11.0.0
|
targetAbi: 10.11.0.0
|
||||||
changelog:
|
changelog:
|
||||||
- Add TV show trailer downloads — separate TMDB sources (Airing Today, On The Air,
|
- Skip trailers and pre-roll/feature pre-roll bumpers the current user
|
||||||
Popular, Top Rated) and a "Max TV show trailers per run" cap independent from
|
can't actually access (e.g. a library not in their library access list)
|
||||||
movies; set either cap to 0 to skip that category entirely
|
— some Cinema Mode clients hang indefinitely trying to play an
|
||||||
- Add trailers before TV episodes via IIntroProvider — only the first episode a
|
inaccessible item, so these are now filtered out per-user instead
|
||||||
user watches each day gets trailers, picked using the show's genre/rating for
|
|
||||||
filtering
|
|
||||||
- Movie and TV trailers are now kept separate — 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
|
|
||||||
|
|
||||||
dotnetProjects:
|
dotnetProjects:
|
||||||
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||||
|
|||||||
@@ -8,6 +8,54 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.4/cinematrailers4jellyfins_1.0.0.4.zip",
|
||||||
|
"timestamp": "2026-06-10T04:42:17Z",
|
||||||
|
"version": "1.0.0.4"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksum": "d43b8d007051d04984d9395d29bc306f",
|
"checksum": "d43b8d007051d04984d9395d29bc306f",
|
||||||
"changelog": "- Add trailer selection filters \u2014 genre match, age rating ceiling, and avoid-repeats cycle; genre and rating are written to each fake-movie NFO at download time from TMDB so Jellyfin can use them for filtering; filters fall back to random when no match is found\n",
|
"changelog": "- Add trailer selection filters \u2014 genre match, age rating ceiling, and avoid-repeats cycle; genre and rating are written to each fake-movie NFO at download time from TMDB so Jellyfin can use them for filtering; filters fall back to random when no match is found\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user