feat: Jellyfin Plugin UI Integration #51
@@ -41,21 +41,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function injectUI() {
|
function injectUI() {
|
||||||
// 1. Item Detail Page - Add icon button for rating
|
// 1. Item Detail Page
|
||||||
const detailButtons = document.querySelector('.mainDetailButtons');
|
const detailButtons = document.querySelector('.mainDetailButtons');
|
||||||
if (detailButtons && !document.querySelector('.btnMovieNightRate')) {
|
if (detailButtons) {
|
||||||
const itemId = getItemIdFromUrl();
|
const itemId = getItemIdFromUrl();
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
|
// MovieNight Rating
|
||||||
|
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.preventDefault(); e.stopPropagation(); showRatingDialog(itemId);
|
||||||
e.stopPropagation();
|
|
||||||
showRatingDialog(itemId);
|
|
||||||
});
|
});
|
||||||
const moreBtn = detailButtons.querySelector('.btnMoreCommands');
|
insertInDetailRow(detailButtons, rateBtn);
|
||||||
if (moreBtn) {
|
}
|
||||||
detailButtons.insertBefore(rateBtn, moreBtn);
|
// Mark Viewed in MovieNight
|
||||||
} else {
|
if (!document.querySelector('.btnMovieNightMarkViewed')) {
|
||||||
detailButtons.appendChild(rateBtn);
|
const viewedBtn = createIconButton('visibility', 'Mark Viewed in MovieNight', 'btnMovieNightMarkViewed', (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation(); submitViewed(itemId);
|
||||||
|
});
|
||||||
|
insertInDetailRow(detailButtons, viewedBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,12 +67,10 @@
|
|||||||
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 && !document.querySelector('.btnMovieNightRecommend')) {
|
if (toolBar && !document.querySelector('.btnMovieNightRecommend')) {
|
||||||
toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => {
|
toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault(); showRecommendation();
|
||||||
showRecommendation();
|
|
||||||
}));
|
}));
|
||||||
toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => {
|
toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault(); showAddMovieDialog();
|
||||||
promptAddMovie();
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,74 +80,135 @@
|
|||||||
const section = document.createElement('div');
|
const section = document.createElement('div');
|
||||||
section.className = 'verticalSection movieNightHomeButtons';
|
section.className = 'verticalSection movieNightHomeButtons';
|
||||||
section.style.padding = '0 var(--sidePadding)';
|
section.style.padding = '0 var(--sidePadding)';
|
||||||
section.innerHTML = '<h2 class="sectionTitle">MovieNight</h2><div class="movieNightBtnContainer" style="display:flex; flex-wrap:wrap;"></div>';
|
section.innerHTML = `
|
||||||
|
<div class="sectionTitleContainer" style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
|
<h2 class="sectionTitle">MovieNight</h2>
|
||||||
|
<span class="movieNightSyncStatus" style="font-size:0.8em; opacity:0.7;"></span>
|
||||||
|
</div>
|
||||||
|
<div class="movieNightBtnContainer" style="display:flex; flex-wrap:wrap; margin-top:0.5em;"></div>
|
||||||
|
`;
|
||||||
const btnContainer = section.querySelector('.movieNightBtnContainer');
|
const btnContainer = section.querySelector('.movieNightBtnContainer');
|
||||||
btnContainer.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', showRecommendation));
|
btnContainer.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', showRecommendation));
|
||||||
btnContainer.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', promptAddMovie));
|
btnContainer.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', showAddMovieDialog));
|
||||||
|
btnContainer.appendChild(createTextButton('Sync Library', 'btnMovieNightSync', triggerSync));
|
||||||
|
|
||||||
homeSections.insertBefore(section, homeSections.firstChild);
|
homeSections.insertBefore(section, homeSections.firstChild);
|
||||||
|
updateSyncStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertInDetailRow(container, btn) {
|
||||||
|
const moreBtn = container.querySelector('.btnMoreCommands');
|
||||||
|
if (moreBtn) container.insertBefore(btn, moreBtn);
|
||||||
|
else container.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
function getItemIdFromUrl() {
|
function getItemIdFromUrl() {
|
||||||
const queryString = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : window.location.search;
|
const queryString = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : window.location.search;
|
||||||
const params = new URLSearchParams(queryString);
|
const params = new URLSearchParams(queryString);
|
||||||
return params.get('id') || params.get('itemId');
|
return params.get('id') || params.get('itemId');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showRatingDialog(itemId) {
|
function createOverlay() {
|
||||||
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 = 'rgba(0,0,0,0.5)';
|
overlay.style.backgroundColor = 'rgba(0,0,0,0.6)';
|
||||||
overlay.style.position = 'fixed';
|
overlay.style.position = 'fixed';
|
||||||
overlay.style.top = '0';
|
overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.bottom = '0';
|
||||||
overlay.style.left = '0';
|
overlay.style.backdropFilter = 'blur(4px)';
|
||||||
overlay.style.right = '0';
|
return overlay;
|
||||||
overlay.style.bottom = '0';
|
}
|
||||||
|
|
||||||
|
function createDialogBase(title) {
|
||||||
const dialog = document.createElement('div');
|
const dialog = document.createElement('div');
|
||||||
dialog.className = 'dialog';
|
dialog.className = 'dialog';
|
||||||
dialog.style.position = 'fixed';
|
dialog.style.position = 'fixed';
|
||||||
dialog.style.top = '50%';
|
dialog.style.top = '50%'; dialog.style.left = '50%';
|
||||||
dialog.style.left = '50%';
|
|
||||||
dialog.style.transform = 'translate(-50%, -50%)';
|
dialog.style.transform = 'translate(-50%, -50%)';
|
||||||
dialog.style.zIndex = '99999';
|
dialog.style.zIndex = '99999';
|
||||||
dialog.style.padding = '2em';
|
dialog.style.padding = '2em';
|
||||||
dialog.style.minWidth = '250px';
|
dialog.style.minWidth = '320px';
|
||||||
dialog.style.backgroundColor = '#222';
|
dialog.style.backgroundColor = '#1a1a1a';
|
||||||
dialog.style.borderRadius = '1em';
|
dialog.style.borderRadius = '1.5em';
|
||||||
dialog.style.color = 'white';
|
dialog.style.color = 'white';
|
||||||
|
dialog.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)';
|
||||||
|
dialog.style.border = '1px solid #333';
|
||||||
|
|
||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<h2 style="margin-top:0; text-align:center;">Rate on MovieNight</h2>
|
<h2 style="margin-top:0; text-align:center; font-weight:400;">${title}</h2>
|
||||||
<div class="rating-grid" style="display:grid; grid-template-columns:repeat(5, 1fr); gap:0.5em; margin:1.5em 0;"></div>
|
<div class="dialog-content" style="margin:1.5em 0;"></div>
|
||||||
<button is="emby-button" class="emby-button button-flat btnCancel" style="width:100%; color: white;">Cancel</button>
|
<div class="dialog-footer" style="display:flex; gap:1em;">
|
||||||
|
<button is="emby-button" class="emby-button button-flat btnCancel" style="flex:1; color: white;">Cancel</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showRatingDialog(itemId) {
|
||||||
|
const overlay = createOverlay();
|
||||||
|
const dialog = createDialogBase('Rate on MovieNight');
|
||||||
|
const content = dialog.querySelector('.dialog-content');
|
||||||
|
|
||||||
|
content.innerHTML = `<div class="rating-grid" style="display:grid; grid-template-columns:repeat(5, 1fr); gap:0.6em;"></div>`;
|
||||||
|
const grid = content.querySelector('.rating-grid');
|
||||||
|
|
||||||
|
const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); };
|
||||||
|
|
||||||
const grid = dialog.querySelector('.rating-grid');
|
|
||||||
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.is = 'emby-button';
|
|
||||||
btn.className = 'emby-button raised';
|
btn.className = 'emby-button raised';
|
||||||
btn.innerText = i;
|
btn.innerText = i;
|
||||||
btn.style.padding = '0.5em';
|
btn.style.padding = '0.8em 0';
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => { cleanup(); await submitRating(itemId, i); };
|
||||||
cleanup();
|
|
||||||
await submitRating(itemId, i);
|
|
||||||
};
|
|
||||||
grid.appendChild(btn);
|
grid.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanup = () => {
|
dialog.querySelector('.btnCancel').onclick = cleanup;
|
||||||
if (overlay.parentNode) document.body.removeChild(overlay);
|
overlay.onclick = (e) => { if (e.target === overlay) cleanup(); };
|
||||||
|
overlay.appendChild(dialog);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showAddMovieDialog() {
|
||||||
|
const overlay = createOverlay();
|
||||||
|
const dialog = createDialogBase('Add Movie (STRM)');
|
||||||
|
const content = dialog.querySelector('.dialog-content');
|
||||||
|
const footer = dialog.querySelector('.dialog-footer');
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div style="margin-bottom:1em;">
|
||||||
|
<label style="display:block; margin-bottom:0.4em; font-size:0.9em; opacity:0.8;">Movie Title</label>
|
||||||
|
<input type="text" class="emby-input txtTitle" style="width:100%; box-sizing:border-box;" placeholder="e.g. Inception">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block; margin-bottom:0.4em; font-size:0.9em; opacity:0.8;">Stream URL (Optional)</label>
|
||||||
|
<input type="text" class="emby-input txtUrl" style="width:100%; box-sizing:border-box;" placeholder="http://...">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const btnAdd = document.createElement('button');
|
||||||
|
btnAdd.className = 'emby-button raised button-submit';
|
||||||
|
btnAdd.style.flex = '2';
|
||||||
|
btnAdd.innerHTML = '<span>Add Film</span>';
|
||||||
|
footer.insertBefore(btnAdd, footer.firstChild);
|
||||||
|
|
||||||
|
const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); };
|
||||||
|
|
||||||
|
btnAdd.onclick = async () => {
|
||||||
|
const title = dialog.querySelector('.txtTitle').value;
|
||||||
|
const url = dialog.querySelector('.txtUrl').value;
|
||||||
|
if (!title) return;
|
||||||
|
cleanup();
|
||||||
|
await addMovie(title, url);
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.querySelector('.btnCancel').onclick = cleanup;
|
dialog.querySelector('.btnCancel').onclick = cleanup;
|
||||||
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);
|
||||||
|
dialog.querySelector('.txtTitle').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showRecommendation() {
|
async function showRecommendation() {
|
||||||
@@ -167,28 +229,46 @@
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to get recommendations', err);
|
console.error('Failed to get recommendations', err);
|
||||||
showMsg('Failed to get recommendations from MovieNight.');
|
showMsg('Failed to get recommendations. Check your API token and MovieNight status.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptAddMovie() {
|
async function addMovie(title, url) {
|
||||||
const title = prompt("Enter movie title:");
|
|
||||||
if (!title) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ApiClient.ajax({
|
await ApiClient.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: ApiClient.getUrl(`MovieNight/Films`),
|
url: ApiClient.getUrl(`MovieNight/Films`),
|
||||||
data: JSON.stringify({ title: title }),
|
data: JSON.stringify({ title, url }),
|
||||||
contentType: 'application/json'
|
contentType: 'application/json'
|
||||||
});
|
});
|
||||||
showMsg(`STRM file created for "${title}". Refresh your library to see it.`);
|
showMsg(`STRM file created for "${title}". Refresh your library to see it.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create movie', err);
|
console.error('Failed to create movie', err);
|
||||||
showMsg('Failed to create movie. Check plugin configuration and logs.');
|
showMsg('Failed to create movie. Ensure STRM output path is configured.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function triggerSync() {
|
||||||
|
try {
|
||||||
|
await ApiClient.ajax({ type: 'POST', url: ApiClient.getUrl(`MovieNight/Sync`) });
|
||||||
|
showMsg('Library sync triggered!');
|
||||||
|
setTimeout(updateSyncStatus, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
showMsg('Failed to trigger sync.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSyncStatus() {
|
||||||
|
const statusEl = document.querySelector('.movieNightSyncStatus');
|
||||||
|
if (!statusEl) return;
|
||||||
|
try {
|
||||||
|
const state = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/SyncState`));
|
||||||
|
if (state && state.lastSyncAt) {
|
||||||
|
statusEl.innerText = `Last sync: ${new Date(state.lastSyncAt).toLocaleString()}`;
|
||||||
|
}
|
||||||
|
} catch (err) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function submitRating(itemId, score) {
|
async function submitRating(itemId, score) {
|
||||||
const userId = ApiClient.getCurrentUserId();
|
const userId = ApiClient.getCurrentUserId();
|
||||||
try {
|
try {
|
||||||
@@ -198,10 +278,24 @@
|
|||||||
data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }),
|
data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }),
|
||||||
contentType: 'application/json'
|
contentType: 'application/json'
|
||||||
});
|
});
|
||||||
showMsg('Rating submitted!');
|
showMsg('Rating submitted to MovieNight!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to submit rating', err);
|
showMsg('Failed to submit rating.');
|
||||||
showMsg('Failed to submit rating to MovieNight.');
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitViewed(itemId) {
|
||||||
|
const userId = ApiClient.getCurrentUserId();
|
||||||
|
try {
|
||||||
|
await ApiClient.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: ApiClient.getUrl(`MovieNight/Users/${userId}/Library/Films/${itemId}/Viewed`),
|
||||||
|
data: JSON.stringify({ watchedAt: new Date().toISOString() }),
|
||||||
|
contentType: 'application/json'
|
||||||
|
});
|
||||||
|
showMsg('Marked as viewed in MovieNight!');
|
||||||
|
} catch (err) {
|
||||||
|
showMsg('Failed to mark as viewed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace Jellyfin.Plugin.MovieNight.Controllers;
|
|||||||
/// Admin endpoints for the MovieNight plugin.
|
/// Admin endpoints for the MovieNight plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
|
||||||
[Route("MovieNight")]
|
[Route("MovieNight")]
|
||||||
|
[Authorize(Policy = "DefaultAuthorization")]
|
||||||
public class MovieNightController : ControllerBase
|
public class MovieNightController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly MovieNightBackendClient _backendClient;
|
private readonly MovieNightBackendClient _backendClient;
|
||||||
@@ -28,6 +28,13 @@ public class MovieNightController : ControllerBase
|
|||||||
_syncService = syncService;
|
_syncService = syncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ping endpoint for connectivity checks.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("Ping")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public ActionResult Ping() => Ok("Pong");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns plugin status.
|
/// Returns plugin status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -50,6 +57,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")]
|
||||||
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);
|
||||||
@@ -61,6 +69,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")]
|
||||||
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);
|
||||||
@@ -73,6 +82,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")]
|
||||||
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);
|
||||||
@@ -124,6 +134,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")]
|
||||||
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;
|
||||||
@@ -143,9 +154,12 @@ 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);
|
||||||
|
|
||||||
// Placeholder content for the .strm file.
|
// Use the provided URL or a placeholder if missing
|
||||||
// In a real scenario, this could be a URL provided in the request.
|
var strmContent = string.IsNullOrWhiteSpace(request.Url)
|
||||||
await System.IO.File.WriteAllTextAsync(filePath, "http://placeholder.url/upload_me_later").ConfigureAwait(false);
|
? "http://placeholder.url/upload_me_later"
|
||||||
|
: request.Url;
|
||||||
|
|
||||||
|
await System.IO.File.WriteAllTextAsync(filePath, strmContent).ConfigureAwait(false);
|
||||||
|
|
||||||
return Ok(new { FilePath = filePath });
|
return Ok(new { FilePath = filePath });
|
||||||
}
|
}
|
||||||
@@ -159,7 +173,7 @@ public class MovieNightController : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create film request.
|
/// Create film request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record CreateFilmRequest(string Title);
|
public sealed record CreateFilmRequest(string Title, string? Url);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rating request.
|
/// Rating request.
|
||||||
|
|||||||
Reference in New Issue
Block a user
itemIdis taken directly from the page URL and then used as{filmId}in the rating POST path. Elsewhere in the plugin (sync/playback events) Jellyfin ids are serialized usingToString("N")(no dashes), so this can lead to inconsistent identifiers being sent to the backend depending on URL format. Consider normalizing the URLidto the same canonical format before calling the API (and applyingencodeURIComponentwhen interpolating path segments).