Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>0.2.1.0</Version>
<AssemblyVersion>0.2.1.0</AssemblyVersion>
<FileVersion>0.2.1.0</FileVersion>
<Version>0.2.1.1</Version>
<AssemblyVersion>0.2.1.1</AssemblyVersion>
<FileVersion>0.2.1.1</FileVersion>
</PropertyGroup>
</Project>
80 changes: 55 additions & 25 deletions Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System;

Check warning on line 1 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 1 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

The file header is missing or not located at the top of the file. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md)

Check warning on line 1 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 1 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

The file header is missing or not located at the top of the file. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md)
using System.Collections.Generic;

Check warning on line 2 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 2 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using System.ComponentModel.DataAnnotations;

Check warning on line 3 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 3 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using System.Linq;

Check warning on line 4 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 4 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using System.Net.Mime;

Check warning on line 5 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 5 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using System.Threading.Tasks;

Check warning on line 6 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 6 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using MediaBrowser.Common.Api;

Check warning on line 7 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)

Check warning on line 7 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Using directive should appear within a namespace declaration (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md)
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
Expand Down Expand Up @@ -220,7 +220,7 @@
trailerInfo = new TrailerInfo
{
Id = movieId, // Use movie ID since remote trailers don't have their own ID
Name = remoteTrailer.Name ?? $"{movie.Name} - Trailer",

Check warning on line 223 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / CodeQL Analysis

Dereference of a possibly null reference.

Check warning on line 223 in Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
Path = remoteTrailer.Url,
RunTimeTicks = null, // Remote trailers typically don't have runtime info
HasSubtitles = false, // Remote trailers typically don't have subtitle info
Expand Down Expand Up @@ -435,7 +435,7 @@
let currentCardElement;
let isPlaying = false;
let resizeHandler;
let attachedCards = new Set(); // Track cards that already have listeners
let attachedCards = new WeakSet(); // Track actual card elements that already have listeners
let mutationDebounce = null;

function log(message, ...args) {{
Expand Down Expand Up @@ -499,15 +499,15 @@
/youtube\.com\/embed\/([^&\?]+)/,
/youtube\.com\/v\/([^&\?]+)/
];

for (const pattern of patterns) {{
const match = url.match(pattern);
if (match && match[1]) {{
log('Extracted YouTube video ID:', match[1]);
return match[1];
}}
}}

log('Failed to extract video ID from URL:', url);
return null;
}}
Expand All @@ -529,7 +529,7 @@
// Apply the same logic as local videos
const youtubeAspectRatio = 16 / 9; // YouTube standard aspect ratio
const cardAspectRatio = cardRect.width / cardRect.height;

if (youtubeAspectRatio > cardAspectRatio) {{
// Video is wider than card, fit to width
containerWidth = cardRect.width;
Expand All @@ -539,11 +539,11 @@
containerHeight = cardRect.height;
containerWidth = Math.round(cardRect.height * youtubeAspectRatio);
}}

// Apply percentage scaling
containerWidth = Math.round(containerWidth * (PREVIEW_SIZE_PERCENTAGE / 100));
containerHeight = Math.round(containerHeight * (PREVIEW_SIZE_PERCENTAGE / 100));

log(`YouTube FitContent dimensions: ${{containerWidth}}x${{containerHeight}} (${{PREVIEW_SIZE_PERCENTAGE}}% of calculated fit)`);
}} else {{
// Manual mode uses configured width/height
Expand Down Expand Up @@ -619,7 +619,7 @@
setTimeout(() => {{
try {{
const volumePercent = ENABLE_PREVIEW_AUDIO ? PREVIEW_VOLUME : 0;

// Set playback quality using IFrame API (2025 method)
if (REMOTE_VIDEO_QUALITY !== 'adaptive') {{
iframe.contentWindow.postMessage(JSON.stringify({{
Expand All @@ -629,7 +629,7 @@
}}), '*');
log('YouTube quality set to: ' + REMOTE_VIDEO_QUALITY);
}}

if (volumePercent === 0) {{
// Keep muted if volume is 0 or audio is disabled
log('YouTube iframe kept muted (volume=0 or audio disabled)');
Expand Down Expand Up @@ -722,7 +722,7 @@
video.muted = !ENABLE_PREVIEW_AUDIO;
video.loop = true;
video.preload = 'metadata';

// Set volume based on configuration (0-100 range converted to 0.0-1.0)
if (ENABLE_PREVIEW_AUDIO) {{
video.volume = PREVIEW_VOLUME / 100.0;
Expand Down Expand Up @@ -770,14 +770,14 @@
// For remote YouTube trailers, convert to embed URL
const youtubeUrl = trailerInfo.Path;
log('Original YouTube URL:', youtubeUrl);

// Extract video ID from YouTube URL
const videoId = extractYouTubeVideoId(youtubeUrl);
if (videoId) {{
// Use youtube-nocookie.com to avoid Error 153 and privacy issues
// Enable JS API for volume control and quality setting
// ALWAYS start muted to prevent loud initial audio, then unmute via API

videoSource = `https://www.youtube-nocookie.com/embed/${{videoId}}?` +
`autoplay=1` +
`&mute=1` + // Always start muted to prevent loud audio spike
Expand Down Expand Up @@ -951,7 +951,7 @@
videoToStop.src = '';
videoToStop.load();
}}

if (iframeToStop) {{
log('Stopping iframe (YouTube)');
iframeToStop.src = 'about:blank';
Expand All @@ -977,21 +977,26 @@
previewToRemove.parentNode.removeChild(previewToRemove);
}}
}}, 300);

}}
}}

function attachHoverListeners() {{
const movieCards = document.querySelectorAll('[data-type=""Movie""], .card[data-itemtype=""Movie""]');

let newCardsCount = 0;

movieCards.forEach(card => {{
// Skip if this card element already has listeners attached
if (attachedCards.has(card)) return;

const movieId = card.getAttribute('data-id') || card.getAttribute('data-itemid');
if (!movieId) return;

// Skip if this card already has listeners attached
if (attachedCards.has(movieId)) return;
if (!movieId) {{
log('Warning: Found movie card without ID');
return;
}}

attachedCards.add(movieId);
// Mark this card element as having listeners
attachedCards.add(card);
newCardsCount++;

card.addEventListener('mouseenter', (e) => {{
Expand All @@ -1016,7 +1021,7 @@
}});

if (newCardsCount > 0) {{
log('Attached hover listeners to', newCardsCount, 'new movie cards');
console.log(`[HoverTrailer] Attached hover listeners to ${{newCardsCount}} new movie cards`);
}}
}}

Expand All @@ -1028,12 +1033,37 @@
}}

// Re-attach listeners when navigation occurs (debounced)
const observer = new MutationObserver(() => {{
// Debounce to prevent excessive re-attachment
clearTimeout(mutationDebounce);
mutationDebounce = setTimeout(() => {{
attachHoverListeners();
}}, 500); // Wait 500ms after last DOM change
const observer = new MutationObserver((mutations) => {{
// Check if any mutations added movie cards
let hasMovieCardChanges = false;
for (const mutation of mutations) {{
if (mutation.addedNodes.length > 0) {{
for (const node of mutation.addedNodes) {{
if (node.nodeType === 1) {{ // Element node
// Check if it's a movie card or contains movie cards
if (node.matches && (node.matches('[data-type=""Movie""]') || node.matches('.card[data-itemtype=""Movie""]'))) {{
hasMovieCardChanges = true;
break;
}}
if (node.querySelector && node.querySelector('[data-type=""Movie""], .card[data-itemtype=""Movie""]')) {{
hasMovieCardChanges = true;
break;
}}
}}
}}
}}
if (hasMovieCardChanges) break;
}}

// Only process if movie cards were added
if (hasMovieCardChanges) {{
// Debounce to prevent excessive re-attachment
clearTimeout(mutationDebounce);
mutationDebounce = setTimeout(() => {{
log('DOM mutation detected, re-attaching listeners...');
attachHoverListeners();
}}, 500);
}}
}});

observer.observe(document.body, {{
Expand Down
42 changes: 0 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,6 @@ https://raw.githubusercontent.com/Fovty/HoverTrailer/master/manifest.json
Access plugin settings through:
**Jellyfin Admin Dashboard** → **Plugins** → **HoverTrailer** → **Settings**

### Hover Preview Customization

| Setting | Description | Default | Range |
|---------|-------------|---------|-------|
| **Horizontal Offset** | X-axis position adjustment | 0px | -2000 to +2000px |
| **Vertical Offset** | Y-axis position adjustment | 0px | -2000 to +2000px |
| **Sizing Mode** | How to size the preview | Fit to Video Content | Fixed/Fit to Content |
| **Width** | Manual width (Fixed mode) | 400px | 100-2000px |
| **Height** | Manual height (Fixed mode) | 225px | 100-2000px |
| **Content Scale** | Percentage scaling (Fit mode) | 200% | 50-1500% |
| **Preview Opacity** | Transparency level | 1.0 | 0.1-1.0 |
| **Border Radius** | Corner rounding | 10px | 0-50px |

### Audio & Debug Settings

| Setting | Description | Default |
|---------|-------------|---------|
| **Enable Preview Audio** | Play audio with trailers | ✅ Enabled |
| **Enable Debug Mode** | Detailed console logging | ✅ Enabled |

## Troubleshooting

### Trailers Not Playing
Expand Down Expand Up @@ -152,18 +132,6 @@ volumes:
- /path/to/jellyfin/config/index.html:/jellyfin/jellyfin-web/index.html
```

**How It Works:**
HoverTrailer uses reflection-based integration to detect File Transformation at runtime. If the plugin is available, HoverTrailer registers a transformation handler that injects the script tag into index.html as it's served to browsers. If File Transformation isn't available, HoverTrailer falls back to direct file modification (which may require permissions as described in Options 2-3).

### Performance Issues
1. **Adjust sizing settings**:
- Lower "Content Scale" percentage
- Use "Fixed" sizing mode for consistent performance

2. **Network considerations**:
- Remote trailers require internet connectivity
- Local trailers provide better performance

## Development

### Building from Source
Expand All @@ -176,16 +144,6 @@ cd hovertrailer
dotnet build --configuration Release --property:TreatWarningsAsErrors=false
```

### Project Structure
```
Fovty.Plugin.HoverTrailer/
├── Api/ # API controllers
├── Configuration/ # Plugin configuration
├── Exceptions/ # Custom exceptions
├── Helpers/ # Utility classes
└── Models/ # Data models
```

## Contributing

1. Fork the repository
Expand Down
Loading