Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e84a897c27 | ||
|
|
3868876401 | ||
|
|
c83d7cbdde | ||
|
|
f1b36f6d0a |
@@ -38,5 +38,10 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration
|
||||
|
||||
/// <summary>Maximum trailers to keep on disk. Oldest are deleted first when exceeded. 0 = unlimited.</summary>
|
||||
public int MaxTotalTrailers { get; set; } = 50;
|
||||
|
||||
// ── IIntroProvider (Cinema Mode / Wholphin) ───────────────────────────
|
||||
|
||||
/// <summary>Number of trailers to inject before each movie via IIntroProvider. 0 = disabled.</summary>
|
||||
public int TrailersPerMovie { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,26 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Cinema Mode Integration -->
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend><h3 class="sectionTitle">Cinema Mode Integration</h3></legend>
|
||||
<p class="fieldDescription">
|
||||
When enabled, this plugin registers as an <strong>IIntroProvider</strong> and
|
||||
injects downloaded trailers before movies — compatible with Jellyfin's built-in
|
||||
cinema mode support and clients like Wholphin.
|
||||
The output folder must be added as a Jellyfin <strong>Movies</strong> library
|
||||
and scanned before trailers appear.
|
||||
</p>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="trailers-per-movie">Trailers per movie</label>
|
||||
<input type="number" id="trailers-per-movie" is="emby-input" min="0" max="10" />
|
||||
<div class="fieldDescription">
|
||||
Number of trailers to play before each movie. Set to 0 to disable.
|
||||
Default: 1.
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Advanced -->
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend><h3 class="sectionTitle">Advanced</h3></legend>
|
||||
@@ -264,6 +284,7 @@
|
||||
document.getElementById('lang-' + code).checked = langs.length === 0 || langs.indexOf(code) !== -1;
|
||||
});
|
||||
document.getElementById('max-total-trailers').value = config.MaxTotalTrailers ?? 50;
|
||||
document.getElementById('trailers-per-movie').value = config.TrailersPerMovie ?? 1;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
@@ -291,6 +312,7 @@
|
||||
// If all are checked treat it as "no preference" (empty string)
|
||||
config.AllowedLanguages = checkedLangs.length === allLangCodes.length ? '' : checkedLangs.join(',');
|
||||
config.MaxTotalTrailers = parseInt(document.getElementById('max-total-trailers').value, 10) || 50;
|
||||
config.TrailersPerMovie = parseInt(document.getElementById('trailers-per-movie').value, 10) || 0;
|
||||
ApiClient.updatePluginConfiguration(pluginId, config)
|
||||
.then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.CinemaTrailers4Jellyfins</RootNamespace>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.2</AssemblyVersion>
|
||||
<FileVersion>1.0.0.2</FileVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks;
|
||||
using Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -15,6 +16,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||
serviceCollection.AddSingleton<TrailerDownloadService>();
|
||||
serviceCollection.AddSingleton<FakeMovieService>();
|
||||
serviceCollection.AddTransient<IScheduledTask, DownloadTrailersTask>();
|
||||
serviceCollection.AddTransient<IIntroProvider, TrailerIntroProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +97,19 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
||||
"-shortest",
|
||||
"-c:v libx264 -tune stillimage -pix_fmt yuv420p",
|
||||
"-c:a aac -b:a 64k",
|
||||
"-f mp4",
|
||||
$"\"{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
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
FileName = ffmpegPath,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
@@ -135,7 +141,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| Failed to generate the master fake-movie file. Is ffmpeg installed and on PATH?");
|
||||
_logger.LogError(ex, "|CinemaTrailers4Jellyfins| Failed to generate the master fake-movie file.");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
|
||||
{
|
||||
public class TrailerIntroProvider : IIntroProvider
|
||||
{
|
||||
private static readonly Random _rng = new();
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<TrailerIntroProvider> _logger;
|
||||
|
||||
public string Name => "CinemaTrailers4Jellyfins";
|
||||
|
||||
public TrailerIntroProvider(ILibraryManager libraryManager, ILogger<TrailerIntroProvider> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
if (config == null || config.TrailersPerMovie <= 0)
|
||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||
|
||||
if (item is not Movie)
|
||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||
|
||||
var outputFolder = config.DownloadFolder?.TrimEnd(
|
||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
if (string.IsNullOrEmpty(outputFolder))
|
||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||
|
||||
// Break potential recursion: don't inject intros before items that live inside
|
||||
// our output folder. This covers both the fake-movie files and their trailer
|
||||
// extras, so Wholphin can't trigger a getIntros loop on the intro item itself.
|
||||
if (item.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true)
|
||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||
|
||||
// Server-scoped query (no User parameter) so library visibility settings don't
|
||||
// prevent trailer discovery even when the output library is hidden from users.
|
||||
var fakeMovies = _libraryManager
|
||||
.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie },
|
||||
Recursive = true,
|
||||
})
|
||||
.OfType<Movie>()
|
||||
.Where(m =>
|
||||
m.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true
|
||||
&& m.LocalTrailers.Count > 0)
|
||||
.ToList();
|
||||
|
||||
if (fakeMovies.Count == 0)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"|CinemaTrailers4Jellyfins| No indexed trailers found in {Folder}. "
|
||||
+ "Ensure the output folder is added as a Jellyfin Movies library and scanned.",
|
||||
outputFolder);
|
||||
return Task.FromResult(Enumerable.Empty<IntroInfo>());
|
||||
}
|
||||
|
||||
var selected = fakeMovies
|
||||
.OrderBy(_ => _rng.Next())
|
||||
.Take(config.TrailersPerMovie)
|
||||
.Select(m =>
|
||||
{
|
||||
var t = m.LocalTrailers.ElementAt(_rng.Next(m.LocalTrailers.Count));
|
||||
return new IntroInfo { ItemId = t.Id, Path = t.Path };
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IEnumerable<IntroInfo>>(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
build.yaml
10
build.yaml
@@ -1,5 +1,5 @@
|
||||
---
|
||||
version: 1.0.0.0
|
||||
version: 1.0.0.2
|
||||
name: CinemaTrailers4Jellyfins
|
||||
guid: b581493e-1046-40ed-b6dc-cb8027624984
|
||||
description: >
|
||||
@@ -12,10 +12,10 @@ category: General
|
||||
owner: 514mart
|
||||
targetAbi: 10.11.0.0
|
||||
changelog:
|
||||
- Initial release based on Trailers4Jellyfin — TMDB/YouTube trailer downloads,
|
||||
scheduled task, language/date filters, and trailer rotation, repackaged so
|
||||
each trailer ships inside its own fake-movie folder (placeholder video +
|
||||
locked NFO + trailer) for use with a Cinema Mode pre-roll plugin
|
||||
- Add IIntroProvider support — downloaded trailers now play as pre-rolls before
|
||||
movies in Jellyfin clients with cinema mode (Wholphin, etc.); configurable
|
||||
trailers-per-movie count; recursion-safe so the intro items themselves never
|
||||
trigger a second round of intro injection
|
||||
|
||||
dotnetProjects:
|
||||
- name: Jellyfin.Plugin.CinemaTrailers4Jellyfins
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
"overview": "Builds a self-contained trailer library for Cinema Mode pre-roll plugins",
|
||||
"owner": "514mart",
|
||||
"imageUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/raw/branch/main/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Images/logo.svg",
|
||||
"versions": []
|
||||
"versions": [
|
||||
{
|
||||
"checksum": "70edda53c848d5481223dda7afc763c2",
|
||||
"changelog": "- Initial release based on Trailers4Jellyfin \u2014 TMDB/YouTube trailer downloads, scheduled task, language/date filters, and trailer rotation, repackaged so each trailer ships inside its own fake-movie folder (placeholder video + locked NFO + trailer) for use with a Cinema Mode pre-roll plugin\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.1/cinematrailers4jellyfins_1.0.0.1.zip",
|
||||
"timestamp": "2026-06-08T20:48:56Z",
|
||||
"version": "1.0.0.1"
|
||||
},
|
||||
{
|
||||
"checksum": "b8a6e246b2f07dadafb196c120dab2f6",
|
||||
"changelog": "- Initial release based on Trailers4Jellyfin \u2014 TMDB/YouTube trailer downloads, scheduled task, language/date filters, and trailer rotation, repackaged so each trailer ships inside its own fake-movie folder (placeholder video + locked NFO + trailer) for use with a Cinema Mode pre-roll plugin\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://www.git.quarantinedstudio.com/mvezina/CinemaTrailers4Jellyfins/releases/download/v1.0.0.0/cinematrailers4jellyfins_1.0.0.0.zip",
|
||||
"timestamp": "2026-06-08T20:31:51Z",
|
||||
"version": "1.0.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user