Refactor Jellyfin plugin: UI integration, backend contract, and STRM creation.
- Integrated "Recommend Film", "Add Movie (STRM)", and Rating UI into Jellyfin web interface. - Implemented full library sync (metadata with poster URLs, user states) with library filtering support. - Added support for creating films via .strm file generation. - Updated MovieNightBackendClient to support sync, recommendations, ratings, and viewed status. - Added proxy endpoints to MovieNightController for frontend-backend communication. - Ensured compatibility with Jellyfin 10.11.x and .NET 9.0. Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
co-authored by
devitq
parent
07c9995c3c
commit
841ce8534f
@@ -1,20 +1,42 @@
|
||||
(function () {
|
||||
const PLUGIN_ID = "42c72919-d6ff-4f62-bb8c-0fac39efafdb";
|
||||
|
||||
function getAlert() {
|
||||
if (typeof Dashboard !== 'undefined' && Dashboard.alert) {
|
||||
return (options) => Dashboard.alert(options);
|
||||
}
|
||||
return (options) => {
|
||||
const msg = typeof options === 'string' ? options : (options.text || options.title);
|
||||
alert(msg);
|
||||
};
|
||||
}
|
||||
|
||||
const showMsg = getAlert();
|
||||
|
||||
function injectUI() {
|
||||
// 1. Inject "Recommend me a film" button in Library views
|
||||
const headerButtons = document.querySelector('.headerViewButtons');
|
||||
if (headerButtons && !document.querySelector('.btnMovieNightRecommend')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'emby-button raised btnMovieNightRecommend';
|
||||
btn.innerHTML = '<span>Recommend Film</span>';
|
||||
btn.style.marginLeft = '1em';
|
||||
btn.onclick = showRecommendation;
|
||||
headerButtons.appendChild(btn);
|
||||
const headerButtons = document.querySelector('.headerViewButtons, .view-library .content-primary, .home-section .sectionTitleContainer');
|
||||
|
||||
if (headerButtons) {
|
||||
if (!document.querySelector('.btnMovieNightRecommend')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'emby-button raised btnMovieNightRecommend';
|
||||
btn.innerHTML = '<span>Recommend Film</span>';
|
||||
btn.style.marginLeft = '1em';
|
||||
btn.onclick = showRecommendation;
|
||||
headerButtons.appendChild(btn);
|
||||
}
|
||||
|
||||
if (!document.querySelector('.btnMovieNightAddMovie')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'emby-button raised btnMovieNightAddMovie';
|
||||
btn.innerHTML = '<span>Add Movie (STRM)</span>';
|
||||
btn.style.marginLeft = '1em';
|
||||
btn.onclick = promptAddMovie;
|
||||
headerButtons.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Inject Rating UI in Item Details
|
||||
const detailButtons = document.querySelector('.itemDetailButtons');
|
||||
const detailButtons = document.querySelector('.itemDetailButtons, .itemDetailsButtons');
|
||||
if (detailButtons && !document.querySelector('.movieNightRatingContainer')) {
|
||||
const itemId = getItemIdFromUrl();
|
||||
if (itemId) {
|
||||
@@ -26,10 +48,12 @@
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.innerText = 'MovieNight: ';
|
||||
label.style.marginRight = '0.5em';
|
||||
container.appendChild(label);
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.className = 'emby-select';
|
||||
select.style.padding = '0.2em';
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
@@ -46,37 +70,46 @@
|
||||
|
||||
function getItemIdFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return normalizeJellyfinId(params.get('id'));
|
||||
}
|
||||
|
||||
function normalizeJellyfinId(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = value.replace(/-/g, '').toLowerCase();
|
||||
return /^[0-9a-f]{32}$/.test(normalized) ? normalized : null;
|
||||
return params.get('id') || params.get('itemId');
|
||||
}
|
||||
|
||||
async function showRecommendation() {
|
||||
const userId = ApiClient.getCurrentUserId();
|
||||
try {
|
||||
const response = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${encodeURIComponent(userId)}/Recommendations`));
|
||||
const response = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Recommendations`));
|
||||
const recommendations = typeof response === 'string' ? JSON.parse(response) : response;
|
||||
|
||||
if (recommendations && recommendations.length > 0) {
|
||||
const rec = recommendations[0];
|
||||
const film = rec.film || rec;
|
||||
Dashboard.alert({
|
||||
showMsg({
|
||||
title: 'MovieNight Recommendation',
|
||||
text: `How about watching: ${film.title}?\n\nReason: ${rec.reasons?.join(', ') || 'Based on your preferences'}`
|
||||
});
|
||||
} else {
|
||||
Dashboard.alert('No recommendations found at the moment.');
|
||||
showMsg('No recommendations found at the moment.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get recommendations', err);
|
||||
Dashboard.alert('Failed to get recommendations from MovieNight.');
|
||||
showMsg('Failed to get recommendations from MovieNight.');
|
||||
}
|
||||
}
|
||||
|
||||
async function promptAddMovie() {
|
||||
const title = prompt("Enter movie title:");
|
||||
if (!title) return;
|
||||
|
||||
try {
|
||||
await ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl(`MovieNight/Films`),
|
||||
data: JSON.stringify({ title: title }),
|
||||
contentType: 'application/json'
|
||||
});
|
||||
showMsg(`STRM file created for "${title}". Refresh your library to see it.`);
|
||||
} catch (err) {
|
||||
console.error('Failed to create movie', err);
|
||||
showMsg('Failed to create movie. Check plugin configuration and logs.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,31 +119,19 @@
|
||||
try {
|
||||
await ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl(`MovieNight/Users/${encodeURIComponent(userId)}/Ratings/Films/${encodeURIComponent(itemId)}`),
|
||||
url: ApiClient.getUrl(`MovieNight/Users/${userId}/Ratings/Films/${itemId}`),
|
||||
data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }),
|
||||
contentType: 'application/json'
|
||||
});
|
||||
Dashboard.alert('Rating submitted!');
|
||||
showMsg('Rating submitted!');
|
||||
} catch (err) {
|
||||
console.error('Failed to submit rating', err);
|
||||
Dashboard.alert('Failed to submit rating to MovieNight.');
|
||||
showMsg('Failed to submit rating to MovieNight.');
|
||||
}
|
||||
}
|
||||
|
||||
let pendingInjection = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (pendingInjection) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingInjection = true;
|
||||
requestAnimationFrame(() => {
|
||||
pendingInjection = false;
|
||||
injectUI();
|
||||
});
|
||||
});
|
||||
const observer = new MutationObserver(injectUI);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Initial call
|
||||
injectUI();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user