(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 createTextButton(text, className, onClick) { const btn = document.createElement('button'); btn.type = 'button'; btn.is = 'emby-button'; btn.className = `emby-button raised ${className}`; btn.style.margin = '0.5em'; btn.style.padding = '0.4em 1em'; btn.innerHTML = `${text}`; btn.onclick = onClick; return btn; } function createIconButton(icon, title, className, onClick) { const btn = document.createElement('button'); btn.type = 'button'; btn.is = 'emby-button'; btn.className = `button-flat detailButton emby-button ${className}`; btn.title = title; btn.innerHTML = `
`; btn.onclick = onClick; return btn; } async function injectUI() { // Check for onboarding await checkOnboarding(); // 1. Item Detail Page const detailButtons = document.querySelector('.mainDetailButtons'); if (detailButtons) { const itemId = getItemIdFromUrl(); if (itemId) { // MovieNight Rating if (!document.querySelector('.btnMovieNightRate')) { const rateBtn = createIconButton('star_rate', 'Rate on MovieNight', 'btnMovieNightRate', (e) => { e.preventDefault(); e.stopPropagation(); showRatingDialog(itemId); }); insertInDetailRow(detailButtons, rateBtn); } // Mark Viewed in MovieNight if (!document.querySelector('.btnMovieNightMarkViewed')) { const viewedBtn = createIconButton('visibility', 'Mark Viewed in MovieNight', 'btnMovieNightMarkViewed', (e) => { e.preventDefault(); e.stopPropagation(); submitViewed(itemId); }); insertInDetailRow(detailButtons, viewedBtn); } } } // 2. Library Pages - Add text buttons to toolbar const toolBar = document.querySelector('.libraryPage:not(.itemDetailPage) .flex.align-items-center.justify-content-center.focuscontainer-x'); if (toolBar && !document.querySelector('.btnMovieNightRecommend')) { toolBar.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', (e) => { e.preventDefault(); showRecommendation(); })); toolBar.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', (e) => { e.preventDefault(); showAddMovieDialog(); })); } // 3. Home Page - Prepend a MovieNight section const homeSections = document.querySelector('.sections.homeSectionsContainer'); if (homeSections && !document.querySelector('.movieNightHomeButtons')) { const section = document.createElement('div'); section.className = 'verticalSection movieNightHomeButtons'; section.style.padding = '0 var(--sidePadding)'; section.innerHTML = `

MovieNight

`; const btnContainer = section.querySelector('.movieNightBtnContainer'); btnContainer.appendChild(createTextButton('Recommend Film', 'btnMovieNightRecommend', showRecommendation)); btnContainer.appendChild(createTextButton('Add Movie (STRM)', 'btnMovieNightAddMovie', showAddMovieDialog)); btnContainer.appendChild(createTextButton('Sync Library', 'btnMovieNightSync', triggerSync)); 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() { const queryString = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : window.location.search; const params = new URLSearchParams(queryString); return params.get('id') || params.get('itemId'); } function createOverlay() { const overlay = document.createElement('div'); overlay.className = 'dialogBackdrop dialogBackdropOpened'; overlay.style.zIndex = '99998'; overlay.style.backgroundColor = 'rgba(0,0,0,0.7)'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.bottom = '0'; overlay.style.backdropFilter = 'blur(8px)'; overlay.style.opacity = '1'; return overlay; } function createDialogBase(title) { const dialog = document.createElement('div'); dialog.className = 'dialog'; dialog.style.position = 'fixed'; dialog.style.top = '50%'; dialog.style.left = '50%'; dialog.style.transform = 'translate(-50%, -50%)'; dialog.style.zIndex = '99999'; dialog.style.padding = '2.5em'; dialog.style.minWidth = '350px'; dialog.style.backgroundColor = '#1a1a1a'; dialog.style.borderRadius = '1.5em'; dialog.style.color = 'white'; dialog.style.boxShadow = '0 20px 50px rgba(0,0,0,0.8)'; dialog.style.border = '1px solid #444'; dialog.style.opacity = '1'; dialog.innerHTML = `

${title}

`; return dialog; } async function showRatingDialog(itemId) { const overlay = createOverlay(); const dialog = createDialogBase('Rate on MovieNight'); const content = dialog.querySelector('.dialog-content'); content.innerHTML = `
`; const grid = content.querySelector('.rating-grid'); const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); }; for (let i = 1; i <= 10; i++) { const btn = document.createElement('button'); btn.type = 'button'; btn.is = 'emby-button'; btn.className = 'emby-button raised'; btn.innerText = i; btn.style.padding = '0.8em 0'; btn.style.textAlign = 'center'; btn.style.display = 'flex'; btn.style.alignItems = 'center'; btn.style.justifyContent = 'center'; btn.style.fontSize = '1.2em'; btn.onclick = async () => { cleanup(); await submitRating(itemId, i); }; grid.appendChild(btn); } dialog.querySelector('.btnCancel').onclick = cleanup; 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 = `
`; const btnAdd = document.createElement('button'); btnAdd.className = 'emby-button raised button-submit'; btnAdd.style.flex = '2'; btnAdd.style.backgroundColor = '#0064d2'; btnAdd.innerHTML = 'Add Film'; footer.insertBefore(btnAdd, footer.firstChild); const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); }; 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, year, imdbId); }; dialog.querySelector('.btnCancel').onclick = cleanup; overlay.onclick = (e) => { if (e.target === overlay) cleanup(); }; overlay.appendChild(dialog); document.body.appendChild(overlay); dialog.querySelector('.txtTitle').focus(); } async function showOnboardingDialog() { const overlay = createOverlay(); const dialog = createDialogBase('Welcome to MovieNight!'); dialog.style.minWidth = '450px'; const content = dialog.querySelector('.dialog-content'); const footer = dialog.querySelector('.dialog-footer'); content.innerHTML = `

Pick your preferences to get better recommendations.

`; const genres = ["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Thriller", "Animation", "Documentary"]; const eras = ["1980s", "1990s", "2000s", "2010s", "2020s"]; const types = ["FILM", "SERIES"]; const selections = { genres: new Set(), eras: new Set(), types: new Set() }; const createChip = (text, container, type) => { const chip = document.createElement('div'); chip.innerText = text; chip.style.cssText = 'padding:0.4em 1em; border-radius:2em; border:1px solid #444; cursor:pointer; font-size:0.9em; transition:all 0.2s;'; chip.onclick = () => { if (selections[type].has(text)) { selections[type].delete(text); chip.style.backgroundColor = 'transparent'; chip.style.borderColor = '#444'; } else { selections[type].add(text); chip.style.backgroundColor = '#0064d2'; chip.style.borderColor = '#0064d2'; } }; container.appendChild(chip); }; genres.forEach(g => createChip(g, content.querySelector('.genre-chips'), 'genres')); eras.forEach(e => createChip(e, content.querySelector('.era-chips'), 'eras')); types.forEach(t => createChip(t, content.querySelector('.type-chips'), 'types')); const btnSave = document.createElement('button'); btnSave.className = 'emby-button raised button-submit'; btnSave.style.flex = '2'; btnSave.style.backgroundColor = '#0064d2'; btnSave.innerHTML = 'Save & Start'; footer.insertBefore(btnSave, footer.firstChild); const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); }; btnSave.onclick = async () => { const payload = { weightedGenres: Object.fromEntries([...selections.genres].map(g => [g, 5])), eras: [...selections.eras], contentTypes: [...selections.types] }; cleanup(); await completeOnboarding(payload); }; dialog.querySelector('.btnCancel').onclick = cleanup; overlay.onclick = (e) => { if (e.target === overlay) cleanup(); }; overlay.appendChild(dialog); document.body.appendChild(overlay); } async function checkOnboarding() { if (window.movieNightOnboardingChecked) return; window.movieNightOnboardingChecked = true; const userId = ApiClient.getCurrentUserId(); if (!userId) return; try { const prefs = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Preferences`)); if (!prefs || (!Object.keys(prefs.weightedGenres || {}).length && !prefs.eras?.length)) { showOnboardingDialog(); } } catch (err) { if (err.status === 404) showOnboardingDialog(); } } async function completeOnboarding(payload) { const userId = ApiClient.getCurrentUserId(); try { await ApiClient.ajax({ type: 'POST', url: ApiClient.getUrl(`MovieNight/Users/${userId}/Onboarding`), data: JSON.stringify(payload), contentType: 'application/json' }); showMsg('Welcome! Your preferences have been saved.'); } catch (err) { showMsg('Failed to save onboarding preferences.'); } } async function showRecommendation() { const userId = ApiClient.getCurrentUserId(); try { 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; showMsg({ title: 'MovieNight Recommendation', text: `How about watching: ${film.title}?\n\nReason: ${rec.reasons?.join(', ') || 'Based on your preferences'}` }); } else { showMsg('No recommendations found at the moment.'); } } catch (err) { console.error('Failed to get recommendations', err); showMsg('Failed to get recommendations. Check your API token and MovieNight status.'); } } 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(data), 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. 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`)); const states = Array.isArray(state) ? state : []; const latest = states .map(s => s.lastSuccessfulSyncAt || s.lastSyncedAt) .filter(Boolean) .sort() .pop(); if (latest) { statusEl.innerText = `Last sync: ${new Date(latest).toLocaleString()}`; } } catch (err) { /* ignore */ } } async function submitRating(itemId, score) { const userId = ApiClient.getCurrentUserId(); try { await ApiClient.ajax({ type: 'POST', url: ApiClient.getUrl(`MovieNight/Users/${userId}/Ratings/Films/${itemId}`), data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }), contentType: 'application/json' }); showMsg('Rating submitted to MovieNight!'); } catch (err) { showMsg('Failed to submit rating.'); } } 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.'); } } let timeout; const throttledInject = () => { if (timeout) return; timeout = setTimeout(() => { injectUI(); timeout = null; }, 100); }; const observer = new MutationObserver(throttledInject); observer.observe(document.body, { childList: true, subtree: true }); injectUI(); })();