Fix UI components, 401 Unauthorized errors, and enhance plugin functionality
- Resolved 401 Unauthorized errors by replacing non-existent AuthorizationPolicies with standard [Authorize] attributes. - Modernized UI integration using updated selectors for Jellyfin 10.11+ (.mainDetailButtons). - Replaced browser prompts with custom, theme-appropriate dialogs for Rating and Adding Movies. - Added "Mark Viewed in MovieNight" functionality and UI button. - Added "Sync Library" button and "Last Sync" status display to the Home page. - Fixed Add Movie (STRM) integration and added support for optional stream URLs. - Improved script reliability with a throttled MutationObserver. Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
co-authored by
devitq
parent
5303b4e092
commit
955e503b28
@@ -16,13 +16,11 @@
|
|||||||
function createTextButton(text, className, onClick) {
|
function createTextButton(text, className, onClick) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.setAttribute('is', 'emby-button');
|
btn.is = 'emby-button';
|
||||||
btn.className = `emby-button raised ${className}`;
|
btn.className = `emby-button raised ${className}`;
|
||||||
btn.style.margin = '0.5em';
|
btn.style.margin = '0.5em';
|
||||||
btn.style.padding = '0.4em 1em';
|
btn.style.padding = '0.4em 1em';
|
||||||
const span = document.createElement('span');
|
btn.innerHTML = `<span>${text}</span>`;
|
||||||
span.textContent = text;
|
|
||||||
btn.appendChild(span);
|
|
||||||
btn.onclick = onClick;
|
btn.onclick = onClick;
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
@@ -30,18 +28,14 @@
|
|||||||
function createIconButton(icon, title, className, onClick) {
|
function createIconButton(icon, title, className, onClick) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.setAttribute('is', 'emby-button');
|
btn.is = 'emby-button';
|
||||||
btn.className = `button-flat detailButton emby-button ${className}`;
|
btn.className = `button-flat detailButton emby-button ${className}`;
|
||||||
btn.title = title;
|
btn.title = title;
|
||||||
btn.setAttribute('aria-label', title);
|
btn.innerHTML = `
|
||||||
const content = document.createElement('div');
|
<div class="detailButton-content">
|
||||||
content.className = 'detailButton-content';
|
<span class="material-icons detailButton-icon ${icon}" aria-hidden="true"></span>
|
||||||
const iconSpan = document.createElement('span');
|
</div>
|
||||||
iconSpan.className = 'material-icons detailButton-icon';
|
`;
|
||||||
iconSpan.setAttribute('aria-hidden', 'true');
|
|
||||||
iconSpan.textContent = icon;
|
|
||||||
content.appendChild(iconSpan);
|
|
||||||
btn.appendChild(content);
|
|
||||||
btn.onclick = onClick;
|
btn.onclick = onClick;
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
@@ -53,14 +47,14 @@
|
|||||||
const itemId = getItemIdFromUrl();
|
const itemId = getItemIdFromUrl();
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
// MovieNight Rating
|
// MovieNight Rating
|
||||||
if (!detailButtons.querySelector('.btnMovieNightRate')) {
|
if (!document.querySelector('.btnMovieNightRate')) {
|
||||||
const rateBtn = createIconButton('star_rate', 'Rate on MovieNight', 'btnMovieNightRate', (e) => {
|
const rateBtn = createIconButton('star_rate', 'Rate on MovieNight', 'btnMovieNightRate', (e) => {
|
||||||
e.preventDefault(); e.stopPropagation(); showRatingDialog(itemId);
|
e.preventDefault(); e.stopPropagation(); showRatingDialog(itemId);
|
||||||
});
|
});
|
||||||
insertInDetailRow(detailButtons, rateBtn);
|
insertInDetailRow(detailButtons, rateBtn);
|
||||||
}
|
}
|
||||||
// Mark Viewed in MovieNight
|
// Mark Viewed in MovieNight
|
||||||
if (!detailButtons.querySelector('.btnMovieNightMarkViewed')) {
|
if (!document.querySelector('.btnMovieNightMarkViewed')) {
|
||||||
const viewedBtn = createIconButton('visibility', 'Mark Viewed in MovieNight', 'btnMovieNightMarkViewed', (e) => {
|
const viewedBtn = createIconButton('visibility', 'Mark Viewed in MovieNight', 'btnMovieNightMarkViewed', (e) => {
|
||||||
e.preventDefault(); e.stopPropagation(); submitViewed(itemId);
|
e.preventDefault(); e.stopPropagation(); submitViewed(itemId);
|
||||||
});
|
});
|
||||||
@@ -71,18 +65,14 @@
|
|||||||
|
|
||||||
// 2. Library Pages - Add text buttons to toolbar
|
// 2. Library Pages - Add text buttons to toolbar
|
||||||
const toolBar = document.querySelector('.libraryPage:not(.itemDetailPage) .flex.align-items-center.justify-content-center.focuscontainer-x');
|
const toolBar = document.querySelector('.libraryPage:not(.itemDetailPage) .flex.align-items-center.justify-content-center.focuscontainer-x');
|
||||||
if (toolBar) {
|
if (toolBar && !document.querySelector('.btnMovieNightRecommend')) {
|
||||||
if (!toolBar.querySelector('.btnMovieNightRecommend')) {
|
|
||||||
toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => {
|
toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => {
|
||||||
e.preventDefault(); showRecommendation();
|
e.preventDefault(); showRecommendation();
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
if (!toolBar.querySelector('.btnMovieNightAddMovie')) {
|
|
||||||
toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => {
|
toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => {
|
||||||
e.preventDefault(); showAddMovieDialog();
|
e.preventDefault(); showAddMovieDialog();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Home Page - Prepend a MovieNight section
|
// 3. Home Page - Prepend a MovieNight section
|
||||||
const homeSections = document.querySelector('.sections.homeSectionsContainer');
|
const homeSections = document.querySelector('.sections.homeSectionsContainer');
|
||||||
@@ -123,7 +113,7 @@
|
|||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'dialogBackdrop dialogBackdropOpened';
|
overlay.className = 'dialogBackdrop dialogBackdropOpened';
|
||||||
overlay.style.zIndex = '99998';
|
overlay.style.zIndex = '99998';
|
||||||
overlay.style.backgroundColor = 'var(--dialog-backdrop, rgba(0,0,0,0.6))';
|
overlay.style.backgroundColor = 'rgba(0,0,0,0.6)';
|
||||||
overlay.style.position = 'fixed';
|
overlay.style.position = 'fixed';
|
||||||
overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.bottom = '0';
|
overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.bottom = '0';
|
||||||
overlay.style.backdropFilter = 'blur(4px)';
|
overlay.style.backdropFilter = 'blur(4px)';
|
||||||
@@ -139,25 +129,19 @@
|
|||||||
dialog.style.zIndex = '99999';
|
dialog.style.zIndex = '99999';
|
||||||
dialog.style.padding = '2em';
|
dialog.style.padding = '2em';
|
||||||
dialog.style.minWidth = '320px';
|
dialog.style.minWidth = '320px';
|
||||||
dialog.style.backgroundColor = 'var(--theme-body-background)';
|
dialog.style.backgroundColor = '#1a1a1a';
|
||||||
dialog.style.borderRadius = '1em';
|
dialog.style.borderRadius = '1.5em';
|
||||||
dialog.style.color = 'var(--theme-body-color)';
|
dialog.style.color = 'white';
|
||||||
dialog.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)';
|
dialog.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)';
|
||||||
dialog.style.border = '1px solid var(--theme-light-btn-border-color, transparent)';
|
dialog.style.border = '1px solid #333';
|
||||||
|
|
||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<h2 class="dialogTitle">${title}</h2>
|
<h2 style="margin-top:0; text-align:center; font-weight:400;">${title}</h2>
|
||||||
<div class="dialog-content"></div>
|
<div class="dialog-content" style="margin:1.5em 0;"></div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer" style="display:flex; gap:1em;">
|
||||||
<button is="emby-button" class="emby-button button-flat btnCancel">Cancel</button>
|
<button is="emby-button" class="emby-button button-flat btnCancel" style="flex:1; color: white;">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const content = dialog.querySelector('.dialog-content');
|
|
||||||
content.style.margin = '1.5em 0';
|
|
||||||
const footer = dialog.querySelector('.dialog-footer');
|
|
||||||
footer.style.display = 'flex';
|
|
||||||
footer.style.gap = '1em';
|
|
||||||
dialog.querySelector('.btnCancel').style.flex = '1';
|
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,19 +149,15 @@
|
|||||||
const overlay = createOverlay();
|
const overlay = createOverlay();
|
||||||
const dialog = createDialogBase('Rate on MovieNight');
|
const dialog = createDialogBase('Rate on MovieNight');
|
||||||
const content = dialog.querySelector('.dialog-content');
|
const content = dialog.querySelector('.dialog-content');
|
||||||
const grid = document.createElement('div');
|
|
||||||
grid.className = 'rating-grid';
|
content.innerHTML = `<div class="rating-grid" style="display:grid; grid-template-columns:repeat(5, 1fr); gap:0.6em;"></div>`;
|
||||||
grid.style.display = 'grid';
|
const grid = content.querySelector('.rating-grid');
|
||||||
grid.style.gridTemplateColumns = 'repeat(5, 1fr)';
|
|
||||||
grid.style.gap = '0.6em';
|
|
||||||
content.appendChild(grid);
|
|
||||||
|
|
||||||
const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); };
|
const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); };
|
||||||
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button'; btn.is = 'emby-button';
|
||||||
btn.setAttribute('is', 'emby-button');
|
|
||||||
btn.className = 'emby-button raised';
|
btn.className = 'emby-button raised';
|
||||||
btn.innerText = i;
|
btn.innerText = i;
|
||||||
btn.style.padding = '0.8em 0';
|
btn.style.padding = '0.8em 0';
|
||||||
@@ -225,7 +205,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
dialog.querySelector('.btnCancel').onclick = cleanup;
|
dialog.querySelector('.btnCancel').onclick = cleanup;
|
||||||
dialog.querySelector('.btnCancel').style.width = '100%';
|
|
||||||
overlay.onclick = (e) => { if (e.target === overlay) cleanup(); };
|
overlay.onclick = (e) => { if (e.target === overlay) cleanup(); };
|
||||||
overlay.appendChild(dialog);
|
overlay.appendChild(dialog);
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Plugin.MovieNight.Services;
|
using Jellyfin.Plugin.MovieNight.Services;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ namespace Jellyfin.Plugin.MovieNight.Controllers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("MovieNight")]
|
[Route("MovieNight")]
|
||||||
[Authorize(Policy = "DefaultAuthorization")]
|
|
||||||
public class MovieNightController : ControllerBase
|
public class MovieNightController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly MovieNightBackendClient _backendClient;
|
private readonly MovieNightBackendClient _backendClient;
|
||||||
@@ -22,7 +22,9 @@ public class MovieNightController : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MovieNightController"/> class.
|
/// Initializes a new instance of the <see cref="MovieNightController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MovieNightController(MovieNightBackendClient backendClient, MovieNightSyncService syncService)
|
public MovieNightController(
|
||||||
|
MovieNightBackendClient backendClient,
|
||||||
|
MovieNightSyncService syncService)
|
||||||
{
|
{
|
||||||
_backendClient = backendClient;
|
_backendClient = backendClient;
|
||||||
_syncService = syncService;
|
_syncService = syncService;
|
||||||
@@ -32,7 +34,6 @@ public class MovieNightController : ControllerBase
|
|||||||
/// Ping endpoint for connectivity checks.
|
/// Ping endpoint for connectivity checks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("Ping")]
|
[HttpGet("Ping")]
|
||||||
[AllowAnonymous]
|
|
||||||
public ActionResult Ping() => Ok("Pong");
|
public ActionResult Ping() => Ok("Pong");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,6 +41,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Status response.</returns>
|
/// <returns>Status response.</returns>
|
||||||
[HttpGet("Status")]
|
[HttpGet("Status")]
|
||||||
|
[Authorize]
|
||||||
public ActionResult<MovieNightPluginStatus> GetStatus()
|
public ActionResult<MovieNightPluginStatus> GetStatus()
|
||||||
{
|
{
|
||||||
var configuration = Plugin.Instance?.Configuration;
|
var configuration = Plugin.Instance?.Configuration;
|
||||||
@@ -57,7 +59,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Connection result.</returns>
|
/// <returns>Connection result.</returns>
|
||||||
[HttpPost("TestConnection")]
|
[HttpPost("TestConnection")]
|
||||||
[Authorize(Policy = "RequiresAdmin")]
|
[Authorize]
|
||||||
public async Task<ActionResult<MovieNightConnectionResult>> TestConnection(CancellationToken cancellationToken)
|
public async Task<ActionResult<MovieNightConnectionResult>> TestConnection(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _backendClient.TestConnectionAsync(cancellationToken).ConfigureAwait(false);
|
return await _backendClient.TestConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -69,7 +71,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Backend response.</returns>
|
/// <returns>Backend response.</returns>
|
||||||
[HttpPost("Sync")]
|
[HttpPost("Sync")]
|
||||||
[Authorize(Policy = "RequiresAdmin")]
|
[Authorize]
|
||||||
public async Task<ActionResult<string>> Sync(CancellationToken cancellationToken)
|
public async Task<ActionResult<string>> Sync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _syncService.PerformSyncAsync(cancellationToken).ConfigureAwait(false);
|
await _syncService.PerformSyncAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -82,7 +84,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Backend response.</returns>
|
/// <returns>Backend response.</returns>
|
||||||
[HttpGet("SyncState")]
|
[HttpGet("SyncState")]
|
||||||
[Authorize(Policy = "RequiresAdmin")]
|
[Authorize]
|
||||||
public async Task<ActionResult<string>> SyncState(CancellationToken cancellationToken)
|
public async Task<ActionResult<string>> SyncState(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
|
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -92,6 +94,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// Gets recommendations for the current user.
|
/// Gets recommendations for the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("Users/{userId}/Recommendations")]
|
[HttpGet("Users/{userId}/Recommendations")]
|
||||||
|
[Authorize]
|
||||||
public async Task<ActionResult<string>> GetRecommendations(
|
public async Task<ActionResult<string>> GetRecommendations(
|
||||||
[FromRoute] string userId,
|
[FromRoute] string userId,
|
||||||
[FromQuery] string? contentType,
|
[FromQuery] string? contentType,
|
||||||
@@ -106,6 +109,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// Posts a rating for a film.
|
/// Posts a rating for a film.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("Users/{userId}/Ratings/Films/{filmId}")]
|
[HttpPost("Users/{userId}/Ratings/Films/{filmId}")]
|
||||||
|
[Authorize]
|
||||||
public async Task<ActionResult> PostRating(
|
public async Task<ActionResult> PostRating(
|
||||||
[FromRoute] string userId,
|
[FromRoute] string userId,
|
||||||
[FromRoute] string filmId,
|
[FromRoute] string filmId,
|
||||||
@@ -120,6 +124,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// Marks a film as viewed.
|
/// Marks a film as viewed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("Users/{userId}/Library/Films/{filmId}/Viewed")]
|
[HttpPost("Users/{userId}/Library/Films/{filmId}/Viewed")]
|
||||||
|
[Authorize]
|
||||||
public async Task<ActionResult> MarkViewed(
|
public async Task<ActionResult> MarkViewed(
|
||||||
[FromRoute] string userId,
|
[FromRoute] string userId,
|
||||||
[FromRoute] string filmId,
|
[FromRoute] string filmId,
|
||||||
@@ -134,7 +139,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// Creates a new film by generating a .strm file.
|
/// Creates a new film by generating a .strm file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("Films")]
|
[HttpPost("Films")]
|
||||||
[Authorize(Policy = "RequiresAdmin")]
|
[Authorize]
|
||||||
public async Task<ActionResult> CreateFilm([FromBody] CreateFilmRequest request)
|
public async Task<ActionResult> CreateFilm([FromBody] CreateFilmRequest request)
|
||||||
{
|
{
|
||||||
var config = Plugin.Instance?.Configuration;
|
var config = Plugin.Instance?.Configuration;
|
||||||
@@ -154,7 +159,6 @@ public class MovieNightController : ControllerBase
|
|||||||
var fileName = $"{safeTitle}.strm";
|
var fileName = $"{safeTitle}.strm";
|
||||||
var filePath = Path.Combine(config.StrmOutputPath, fileName);
|
var filePath = Path.Combine(config.StrmOutputPath, fileName);
|
||||||
|
|
||||||
// Use the provided URL or a placeholder if missing
|
|
||||||
var strmContent = string.IsNullOrWhiteSpace(request.Url)
|
var strmContent = string.IsNullOrWhiteSpace(request.Url)
|
||||||
? "http://placeholder.url/upload_me_later"
|
? "http://placeholder.url/upload_me_later"
|
||||||
: request.Url;
|
: request.Url;
|
||||||
|
|||||||
Reference in New Issue
Block a user