// Jellyfin Watchlist Script // Adds watchlist functionality throughout Jellyfin interface // Requires: cardBuilder.js module to be loaded before this script // Requirement #2: Custom Tabs plugin /* In the Custom Tabs plugin, add a new tab with the following HTML content:
Press the button to add an item to your Watchlist
`; // Find the watchlist container and add the message const watchlistContainer = document.querySelector('.sections.watchlist'); if (watchlistContainer) { watchlistContainer.appendChild(emptyMessage); } else { // Fallback to body if watchlist container not found document.body.appendChild(emptyMessage); } } function hideEmptyWatchlistMessage() { const emptyMessage = document.querySelector('.watchlist-empty-message'); if (emptyMessage) { emptyMessage.style.display = 'none'; } } LOG('Watchlist functionality initialized'); /************ Watchlist Button Observer ************/ // Function to add watchlist button to a card overlay container function addWatchlistButton(overlayContainer) { // Check if watchlist button already exists if (overlayContainer && overlayContainer.querySelector('.watchlist-button')) { return; } // Find the card parent to get the item ID const card = overlayContainer.closest('.card'); if (!card) { WARN('Could not find card parent for overlay container'); return; } const itemId = card.getAttribute('data-id'); if (!itemId) { WARN('Could not find data-id on card element'); return; } // Check if card has data-type attribute - if not, don't add watchlist button const itemType = card.getAttribute('data-type'); if (!itemType) { LOG('Card has no data-type, skipping watchlist button'); return; } // Find the .cardOverlayButton-br container const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br'); if (!buttonContainer) { WARN('Could not find .cardOverlayButton-br container'); return; } // Create watchlist button const watchlistButton = document.createElement('button'); watchlistButton.type = 'button'; watchlistButton.className = 'watchlist-button cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button'; watchlistButton.setAttribute('data-action', 'none'); watchlistButton.setAttribute('data-id', itemId); watchlistButton.setAttribute('data-active', 'false'); watchlistButton.title = 'Add to Watchlist'; // Create the bookmark icon const watchlistIcon = document.createElement('span'); watchlistIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover watchlist'; watchlistIcon.setAttribute('aria-hidden', 'true'); watchlistButton.appendChild(watchlistIcon); // Add click event listener watchlistButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Toggle watchlist status const newRating = watchlistButton.dataset.active === 'false' ? 'true' : 'false'; ApiClient.updateUserItemRating(ApiClient.getCurrentUserId(), itemId, newRating); watchlistButton.dataset.active = newRating; // Update icon and title based on state const isActive = watchlistButton.dataset.active === 'true'; // Icon state is handled by CSS class, no need to change textContent watchlistButton.title = isActive ? 'Remove from Watchlist' : 'Add to Watchlist'; }); // Check if item type is supported for watchlist if (itemType !== "Movie" && itemType !== "Series" && itemType !== "Season" && itemType !== "Episode") { LOG('Item type not supported for watchlist:', itemType); return; } // Check item's current watchlist status and set initial state ApiClient.getItem(ApiClient.getCurrentUserId(), itemId).then((item) => { if (item.UserData && item.UserData.Likes) { watchlistButton.dataset.active = 'true'; // Icon state is handled by CSS class, no need to change textContent watchlistButton.title = 'Remove from Watchlist'; } }).catch(err => { ERR('Error fetching item data for watchlist button:', err); }); // Add the watchlist button to the button container buttonContainer.appendChild(watchlistButton); } // Function to process all existing overlay containers function processExistingOverlayContainers() { const overlayContainers = document.querySelectorAll('.cardOverlayContainer'); overlayContainers.forEach((overlayContainer) => { const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br'); if (buttonContainer) { addWatchlistButton(overlayContainer); } }); } // Set up MutationObserver to watch for new overlay containers function setupWatchlistButtonObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // Check for added nodes mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the added node is an overlay container if (node.classList && node.classList.contains('cardOverlayContainer')) { const buttonContainer = node.querySelector('.cardOverlayButton-br'); if (buttonContainer) { addWatchlistButton(node); } } // Check for overlay containers within the added node const overlayContainers = node.querySelectorAll && node.querySelectorAll('.cardOverlayContainer'); if (overlayContainers && overlayContainers.length > 0) { overlayContainers.forEach((overlayContainer) => { const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br'); if (buttonContainer) { addWatchlistButton(overlayContainer); } }); } } }); } }); }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); // Process any existing overlay containers processExistingOverlayContainers(); } // Initialize the observer setupWatchlistButtonObserver(); /************ Item Detail Page Observer ************/ // Function to add watchlist button to item detail page function addDetailPageWatchlistButton() { const trailerButton = document.querySelector('.itemDetailPage:not(.hide) .btnPlayTrailer'); if (!trailerButton) { return; } // Check if watchlist button already exists const existingButton = document.querySelector('.itemDetailPage:not(.hide) .watchlist-icon'); if (existingButton) { return; } const watchlistIcon = document.createElement('button'); watchlistIcon.setAttribute("is", "emby-button"); watchlistIcon.className = 'watchlist-icon detailButton emby-button hide'; watchlistIcon.title = "Add to Watchlist"; watchlistIcon.dataset.active = 'false'; // Add the content wrapper and icon span inside the button const contentWrapper = document.createElement('div'); contentWrapper.className = 'detailButton-content'; const iconSpan = document.createElement('span'); iconSpan.className = 'material-icons detailButton-icon watchlist'; iconSpan.setAttribute('aria-hidden', 'true'); contentWrapper.appendChild(iconSpan); watchlistIcon.appendChild(contentWrapper); // Get item ID from URL const itemId = window.location.href.substring(window.location.href.indexOf("id=") + 3, window.location.href.indexOf("id=") + 35); watchlistIcon.addEventListener('click', () => { const newRating = watchlistIcon.dataset.active === 'false' ? 'true' : 'false'; ApiClient.updateUserItemRating(ApiClient.getCurrentUserId(), itemId, newRating); watchlistIcon.dataset.active = newRating; // Update title based on state const isActive = watchlistIcon.dataset.active === 'true'; watchlistIcon.title = isActive ? 'Remove from Watchlist' : 'Add to Watchlist'; }); // Get item data to check if it should be shown and current state ApiClient.getItem(ApiClient.getCurrentUserId(), itemId).then((item) => { // Only show for Movies, Series, Seasons, and Episodes if (item.Type !== "Movie" && item.Type !== "Series" && item.Type !== "Season" && item.Type !== "Episode") { watchlistIcon.style.display = "none"; return; } // Set initial state based on current watchlist status if (item.UserData && item.UserData.Likes) { watchlistIcon.dataset.active = 'true'; watchlistIcon.title = 'Remove from Watchlist'; } }).catch(err => { ERR('Error fetching item data:', err); }); // Add button after trailer button trailerButton.after(watchlistIcon); // Set up observer to show button when other buttons become visible const target = document.querySelector('.mainDetailButtons'); if (target) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === "attributes" && mutation.attributeName === "class" && mutation.target.classList.contains("emby-button") && !mutation.target.classList.contains("watchlist-icon") && !mutation.target.classList.contains("hide")) { if (watchlistIcon && watchlistIcon.classList.contains("hide")) { watchlistIcon.classList.remove('hide'); observer.disconnect(); } } }); }); observer.observe(target, { attributes: true, subtree: true, attributeFilter: ["class"] }); } } // Function to monitor item detail pages function monitorItemDetailPage() { const observer = new MutationObserver(() => { const visibleItemDetailPage = document.querySelector('.itemDetailPage:not(.hide)'); if (!visibleItemDetailPage) { return; } // Check if watchlist button already exists const existingButton = document.querySelector('.itemDetailPage:not(.hide) .watchlist-icon'); if (existingButton) { return; } // Get item ID from URL to check type before adding button const itemId = window.location.href.substring(window.location.href.indexOf("id=") + 3, window.location.href.indexOf("id=") + 35); if (!itemId) { return; } // Check if item type is supported for watchlist ApiClient.getItem(ApiClient.getCurrentUserId(), itemId).then((item) => { if (item.Type === "Movie" || item.Type === "Series" || item.Type === "Season" || item.Type === "Episode") { addDetailPageWatchlistButton(); } }).catch(err => { ERR('Error fetching item data for detail page type check:', err); }); }); // Start observing the entire document body observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); } // Initialize the item detail page observer monitorItemDetailPage(); // Debug functions for troubleshooting (available in console) window.debugWatchlistButtons = function() { LOG('Manual debug trigger called'); LOG('Current overlay containers:', document.querySelectorAll('.cardOverlayContainer').length); LOG('Current watchlist buttons:', document.querySelectorAll('.watchlist-button').length); processExistingOverlayContainers(); }; window.debugWatchlistRendering = function() { LOG('Manual watchlist rendering trigger called'); checkAndRenderWatchlist(); }; window.debugDetailPageWatchlist = function() { LOG('Manual detail page watchlist trigger called'); addDetailPageWatchlistButton(); }; LOG('Debug functions available: window.debugWatchlistButtons(), window.debugWatchlistRendering(), window.debugDetailPageWatchlist()'); })();