feat: Jellyfin Plugin UI Integration #51
@@ -185,12 +185,22 @@
|
||||
const footer = dialog.querySelector('.dialog-footer');
|
||||
|
||||
content.innerHTML = `
|
||||
<div style="margin-bottom:1.5em;">
|
||||
<label style="display:block; margin-bottom:0.6em; font-size:1em; opacity:0.9; color:white;">Movie Title</label>
|
||||
<div style="margin-bottom:1em;">
|
||||
<label style="display:block; margin-bottom:0.3em; font-size:0.9em; opacity:0.8; color:white;">Movie Title (Required)</label>
|
||||
<input type="text" class="emby-input txtTitle" style="width:100%; box-sizing:border-box; background:#333; border:1px solid #555; color:white; padding:0.6em;" placeholder="e.g. Inception">
|
||||
</div>
|
||||
<div style="display:flex; gap:1em; margin-bottom:1em;">
|
||||
<div style="flex:1;">
|
||||
<label style="display:block; margin-bottom:0.3em; font-size:0.9em; opacity:0.8; color:white;">Year</label>
|
||||
<input type="number" class="emby-input txtYear" style="width:100%; box-sizing:border-box; background:#333; border:1px solid #555; color:white; padding:0.6em;" placeholder="2010">
|
||||
</div>
|
||||
<div style="flex:2;">
|
||||
<label style="display:block; margin-bottom:0.3em; font-size:0.9em; opacity:0.8; color:white;">IMDb ID</label>
|
||||
<input type="text" class="emby-input txtImdb" style="width:100%; box-sizing:border-box; background:#333; border:1px solid #555; color:white; padding:0.6em;" placeholder="tt1375666">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block; margin-bottom:0.6em; font-size:1em; opacity:0.9; color:white;">Stream URL (Optional)</label>
|
||||
<label style="display:block; margin-bottom:0.3em; font-size:0.9em; opacity:0.8; color:white;">Stream URL (Optional)</label>
|
||||
<input type="text" class="emby-input txtUrl" style="width:100%; box-sizing:border-box; background:#333; border:1px solid #555; color:white; padding:0.6em;" placeholder="http://...">
|
||||
</div>
|
||||
`;
|
||||
@@ -206,10 +216,12 @@
|
||||
|
||||
btnAdd.onclick = async () => {
|
||||
const title = dialog.querySelector('.txtTitle').value;
|
||||
const year = dialog.querySelector('.txtYear').value;
|
||||
const imdbId = dialog.querySelector('.txtImdb').value;
|
||||
const url = dialog.querySelector('.txtUrl').value;
|
||||
if (!title) return;
|
||||
cleanup();
|
||||
await addMovie(title, url);
|
||||
await addMovie(title, url, year, imdbId);
|
||||
};
|
||||
|
||||
dialog.querySelector('.btnCancel').onclick = cleanup;
|
||||
@@ -241,12 +253,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function addMovie(title, url) {
|
||||
async function addMovie(title, url, year, imdbId) {
|
||||
try {
|
||||
const data = { title, url };
|
||||
if (year) data.year = parseInt(year);
|
||||
if (imdbId) data.imdbId = imdbId;
|
||||
|
||||
await ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl(`MovieNight/Films`),
|
||||
data: JSON.stringify({ title, url }),
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json'
|
||||
});
|
||||
showMsg(`STRM file created for "${title}". Refresh your library to see it.`);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.MovieNight.Services;
|
||||
@@ -136,7 +138,8 @@ public class MovieNightController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new film by generating a .strm file.
|
||||
/// Creates a new film by generating a .strm file in a folder-per-movie structure.
|
||||
/// Structure: Movie Name (Year) [imdbid-ttXXXXXXX]/Movie Name (Year) [imdbid-ttXXXXXXX].strm
|
||||
/// </summary>
|
||||
[HttpPost("Films")]
|
||||
[Authorize]
|
||||
@@ -148,24 +151,45 @@ public class MovieNightController : ControllerBase
|
||||
return BadRequest("STRM output path is not configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
return BadRequest("Movie title is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(config.StrmOutputPath))
|
||||
// Construct name: "Movie Name (Year) [imdbid-ttXXXXXXX]"
|
||||
var folderName = request.Title.Trim();
|
||||
if (request.Year.HasValue)
|
||||
{
|
||||
Directory.CreateDirectory(config.StrmOutputPath);
|
||||
folderName += $" ({request.Year})";
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(request.ImdbId))
|
||||
{
|
||||
var ttId = request.ImdbId.Trim().ToLowerInvariant();
|
||||
if (!ttId.StartsWith("tt")) ttId = "tt" + ttId;
|
||||
|
|
||||
folderName += $" [imdbid-{ttId}]";
|
||||
}
|
||||
|
||||
var safeTitle = string.Join("_", request.Title.Split(Path.GetInvalidFileNameChars()));
|
||||
var fileName = $"{safeTitle}.strm";
|
||||
var filePath = Path.Combine(config.StrmOutputPath, fileName);
|
||||
// Sanitize for file system
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var safeFolderName = new string(folderName.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray());
|
||||
|
||||
var movieDirectory = Path.Combine(config.StrmOutputPath, safeFolderName);
|
||||
if (!Directory.Exists(movieDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(movieDirectory);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(movieDirectory, $"{safeFolderName}.strm");
|
||||
|
||||
var strmContent = string.IsNullOrWhiteSpace(request.Url)
|
||||
? "http://placeholder.url/upload_me_later"
|
||||
: request.Url;
|
||||
: request.Url.Trim();
|
||||
|
||||
await System.IO.File.WriteAllTextAsync(filePath, strmContent).ConfigureAwait(false);
|
||||
|
||||
return Ok(new { FilePath = filePath });
|
||||
return Ok(new { FilePath = filePath, FolderName = safeFolderName });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -177,7 +201,7 @@ public class MovieNightController : ControllerBase
|
||||
/// <summary>
|
||||
/// Create film request.
|
||||
/// </summary>
|
||||
public sealed record CreateFilmRequest(string Title, string? Url);
|
||||
public sealed record CreateFilmRequest(string Title, string? Url, int? Year, string? ImdbId);
|
||||
|
||||
/// <summary>
|
||||
/// Rating request.
|
||||
|
||||
Reference in New Issue
Block a user
These endpoints are documented as operating on the “current user”, but they accept an arbitrary
userIdin the route and do not validate it against the authenticated Jellyfin user. Any authenticated user could request recommendations/submit ratings/mark viewed on behalf of another user by changing the URL. Prefer deriving the user id from the auth context (and removing the route param) or explicitly rejecting requests whereuserIddoesn’t match the authenticated principal.