diff --git a/slideshowpure.js b/slideshowpure.js
index ff76cf2..ee38608 100644
--- a/slideshowpure.js
+++ b/slideshowpure.js
@@ -1,332 +1,796 @@
-const getJellyfinCredentials = () => {
- const jellyfinCreds = localStorage.getItem("jellyfin_credentials");
- const deviceID = localStorage.getItem("_deviceId2");
- if (!jellyfinCreds) {
- console.error("No Jellyfin credentials found in localStorage");
- return;
- }
- try {
- const serverCredentials = JSON.parse(jellyfinCreds);
- const firstServer = serverCredentials?.Servers?.[0];
- if (!firstServer || !firstServer.AccessToken || !firstServer.UserId) {
- console.error("Invalid Jellyfin credentials structure");
- return;
- }
- return {
- token: firstServer.AccessToken,
- userId: firstServer.UserId,
- deviceID
- };
- } catch (e) {
- console.error("Could not parse Jellyfin credentials", e);
+/*
+ * Jellyfin Slideshow by M0RPH3US - Core Module
+ */
+
+// Configuration
+const CONFIG = {
+ IMAGE_URLS: {
+ imdbLogo: 'https://upload.wikimedia.org/wikipedia/commons/6/69/IMDB_Logo_2016.svg',
+ tomatoLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Rotten_Tomatoes_positive_audience.svg/1920px-Rotten_Tomatoes_positive_audience.svg.png',
+ freshTomato: 'https://i.imgur.com/iMfwDk7.png',
+ rottenTomato: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Rotten_Tomatoes_rotten.svg/1024px-Rotten_Tomatoes_rotten.svg.png'
+ },
+ shuffleInterval: 8000,
+ retryInterval: 500,
+ minSwipeDistance: 50,
+ loadingCheckInterval: 100,
+ maxPlotLength: 360,
+ maxMovies: 15,
+ maxTvShows: 15,
+ maxItems: 500,
+ preloadCount: 2,
+ fadeTransitionDuration: 500
+};
+
+// State management
+const STATE = {
+ jellyfinData: {
+ userId: null,
+ appName: null,
+ appVersion: null,
+ deviceName: null,
+ deviceId: null,
+ accessToken: null,
+ serverAddress: null
+ },
+ slideshow: {
+ hasInitialized: false,
+ isTransitioning: false,
+ currentSlideIndex: 0,
+ focusedSlide: null,
+ containerFocused: false,
+ slideInterval: null,
+ itemIds: [],
+ loadedItems: {},
+ createdSlides: {},
+ totalItems: 0,
+ isLoading: false
}
};
-const credentials = getJellyfinCredentials();
-if (credentials) {
- console.log("Token:", credentials.token, "UserId:", credentials.userId, "DeviceID:", credentials.deviceID);
-}
+// Request throttling system
+const requestQueue = [];
+let isProcessingQueue = false;
+
+/**
+ * Process the next request in the queue with throttling
+ */
+const processNextRequest = () => {
+ if (requestQueue.length === 0) {
+ isProcessingQueue = false;
+ return;
+ }
+
+ isProcessingQueue = true;
+ const { url, callback } = requestQueue.shift();
+
+ fetch(url)
+ .then(response => {
+ if (response.ok) {
+ return response;
+ }
+ throw new Error(`Failed to fetch: ${response.status}`);
+ })
+ .then(callback)
+ .catch(error => {
+ console.error("Error in throttled request:", error);
+ })
+ .finally(() => {
+ setTimeout(processNextRequest, 100);
+ });
+};
+
+/**
+ * Add a request to the throttled queue
+ * @param {string} url - URL to fetch
+ * @param {Function} callback - Callback to run on successful fetch
+ */
+const addThrottledRequest = (url, callback) => {
+ requestQueue.push({ url, callback });
+ if (!isProcessingQueue) {
+ processNextRequest();
+ }
+};
+
+/**
+ * Checks if the user is currently logged in
+ * @returns {boolean} True if logged in, false otherwise
+ */
+
+const isUserLoggedIn = () => {
+ try {
+ return (
+ window.ApiClient &&
+ window.ApiClient._currentUser &&
+ window.ApiClient._currentUser.Id &&
+ window.ApiClient._serverInfo &&
+ window.ApiClient._serverInfo.AccessToken
+ );
+ } catch (error) {
+ console.error("Error checking login status:", error);
+ return false;
+ }
+};
+
+/**
+ * Initializes Jellyfin data from ApiClient
+ * @param {Function} callback - Function to call once data is initialized
+ */
+const initJellyfinData = (callback) => {
+ if (!window.ApiClient) {
+ console.warn("⏳ window.ApiClient is not available yet. Retrying...");
+ setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
+ return;
+ }
+
+ try {
+ const apiClient = window.ApiClient;
+ STATE.jellyfinData = {
+ userId: apiClient._currentUser?.Id ||
+ apiClient._serverInfo?.UserId ||
+ apiClient.getCurrentUserId?.() ||
+ apiClient.currentUser?.Id ||
+ "Not Found",
+ appName: apiClient._appName || "Not Found",
+ appVersion: apiClient._appVersion || "Not Found",
+ deviceName: apiClient._deviceName || "Not Found",
+ deviceId: apiClient._deviceId || "Not Found",
+ accessToken: apiClient._serverInfo?.AccessToken || "Not Found",
+ serverAddress: apiClient._serverAddress || "Not Found"
+ };
+ if (callback && typeof callback === 'function') {
+ callback();
+ }
+ } catch (error) {
+ console.error("Error initializing Jellyfin data:", error);
+ setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
+ }
+};
+
+/**
+ * Creates and displays loading screen
+ */
const initLoadingScreen = () => {
const currentPath = window.location.href.toLowerCase();
- if (
+ const isHomePage =
currentPath.includes("/web/#/home.html") ||
currentPath.includes("/web/index.html#/home.html") ||
- currentPath.endsWith("/web/")
- ) {
- const loadingHTML = `
-
-
-
-
-
-
`;
- document.body.insertAdjacentHTML("beforeend", loadingHTML);
+ currentPath.endsWith("/web/");
- const interval = setInterval(() => {
- if (
- document.querySelector(".manualLoginForm") ||
- (
- document.querySelector(".homeSectionsContainer") &&
- document.querySelector(".slide.active")
- )
- ) {
- $(".bar-loading").fadeOut(700, () => $(".bar-loading").remove());
- clearInterval(interval);
+ if (!isHomePage) return;
+
+ const loadingDiv = document.createElement('div');
+ loadingDiv.className = "bar-loading";
+ loadingDiv.id = "page-loader";
+ loadingDiv.innerHTML = `
+
+
+
+
+ `;
+ document.body.appendChild(loadingDiv);
+
+ const checkInterval = setInterval(() => {
+ const loginFormLoaded = document.querySelector(".manualLoginForm");
+ const homePageLoaded =
+ document.querySelector(".homeSectionsContainer") &&
+ document.querySelector(".slide");
+
+ if (loginFormLoaded || homePageLoaded) {
+ const loader = document.querySelector('.bar-loading');
+ if (loader && typeof $ !== 'undefined') {
+ $(loader).fadeOut(700, () => {
+ loader.remove();
+ });
+ } else if (loader) {
+ loader.style.opacity = 0;
+ loader.style.transition = 'opacity 700ms';
+ setTimeout(() => loader.remove(), 700);
}
- }, 100);
+ clearInterval(checkInterval);
+ }
+ }, CONFIG.loadingCheckInterval);
+};
+
+/**
+ * Resets the slideshow state completely
+ */
+const resetSlideshowState = () => {
+ console.log("🔄 Resetting slideshow state...");
+
+ if (STATE.slideshow.slideInterval) {
+ STATE.slideshow.slideInterval.stop();
+ }
+
+ const container = document.getElementById("slides-container");
+ if (container) {
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+ }
+
+ STATE.slideshow.hasInitialized = false;
+ STATE.slideshow.isTransitioning = false;
+ STATE.slideshow.currentSlideIndex = 0;
+ STATE.slideshow.focusedSlide = null;
+ STATE.slideshow.containerFocused = false;
+ STATE.slideshow.slideInterval = null;
+ STATE.slideshow.itemIds = [];
+ STATE.slideshow.loadedItems = {};
+ STATE.slideshow.createdSlides = {};
+ STATE.slideshow.totalItems = 0;
+ STATE.slideshow.isLoading = false;
+};
+
+/**
+ * Watches for login status changes
+ */
+const startLoginStatusWatcher = () => {
+ let wasLoggedIn = false;
+
+ setInterval(() => {
+ const isLoggedIn = isUserLoggedIn();
+
+ if (isLoggedIn !== wasLoggedIn) {
+ if (isLoggedIn) {
+ console.log("👤 User logged in. Initializing slideshow...");
+ if (!STATE.slideshow.hasInitialized) {
+ waitForApiClientAndInitialize();
+ } else {
+ console.log("🔄 Slideshow already initialized, skipping");
+ }
+ } else {
+ console.log("👋 User logged out. Stopping slideshow...");
+ resetSlideshowState();
+ }
+ wasLoggedIn = isLoggedIn;
+ }
+ }, 2000);
+};
+
+/**
+ * Wait for ApiClient to initialize before starting the slideshow
+ */
+const waitForApiClientAndInitialize = () => {
+ if (window.slideshowCheckInterval) {
+ clearInterval(window.slideshowCheckInterval);
+ }
+
+ window.slideshowCheckInterval = setInterval(() => {
+ if (!window.ApiClient) {
+ console.log("⏳ ApiClient not available yet. Waiting...");
+ return;
+ }
+
+ if (
+ window.ApiClient._currentUser &&
+ window.ApiClient._currentUser.Id &&
+ window.ApiClient._serverInfo &&
+ window.ApiClient._serverInfo.AccessToken
+ ) {
+ console.log("🔓 User is fully logged in. Starting slideshow initialization...");
+ clearInterval(window.slideshowCheckInterval);
+
+ if (!STATE.slideshow.hasInitialized) {
+ initJellyfinData(() => {
+ console.log("✅ Jellyfin API client initialized successfully");
+ slidesInit();
+ });
+ } else {
+ console.log("🔄 Slideshow already initialized, skipping");
+ }
+ } else {
+ console.log("🔒 Authentication incomplete. Waiting for complete login...");
+ }
+ }, CONFIG.retryInterval);
+};
+
+waitForApiClientAndInitialize();
+
+/**
+ * Utility functions for slide creation and management
+ */
+const SlideUtils = {
+ /**
+ * Shuffles array elements randomly
+ * @param {Array} array - Array to shuffle
+ * @returns {Array} Shuffled array
+ */
+ shuffleArray(array) {
+ const newArray = [...array];
+ for (let i = newArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
+ }
+ return newArray;
+ },
+
+ /**
+ * Truncates text to specified length and adds ellipsis
+ * @param {HTMLElement} element - Element containing text to truncate
+ * @param {number} maxLength - Maximum length before truncation
+ */
+ truncateText(element, maxLength) {
+ if (!element) return;
+
+ const text = element.innerText || element.textContent;
+ if (text && text.length > maxLength) {
+ element.innerText = text.substring(0, maxLength) + "...";
+ }
+ },
+
+ /**
+ * Creates a separator icon element
+ * @returns {HTMLElement} Separator element
+ */
+ createSeparator() {
+ const separator = document.createElement('i');
+ separator.className = "material-icons radio_button_off separator-icon";
+ return separator;
+ },
+
+ /**
+ * Creates a DOM element with attributes and properties
+ * @param {string} tag - Element tag name
+ * @param {Object} attributes - Element attributes
+ * @param {string|HTMLElement} [content] - Element content
+ * @returns {HTMLElement} Created element
+ */
+ createElement(tag, attributes = {}, content = null) {
+ const element = document.createElement(tag);
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ if (key === 'style' && typeof value === 'object') {
+ Object.entries(value).forEach(([prop, val]) => {
+ element.style[prop] = val;
+ });
+ } else if (key === 'className') {
+ element.className = value;
+ } else if (key === 'innerHTML') {
+ element.innerHTML = value;
+ } else if (key === 'onclick' && typeof value === 'function') {
+ element.addEventListener('click', value);
+ } else {
+ element.setAttribute(key, value);
+ }
+ });
+
+ if (content) {
+ if (typeof content === 'string') {
+ element.textContent = content;
+ } else {
+ element.appendChild(content);
+ }
+ }
+
+ return element;
+ },
+
+ /**
+ * Find or create the slides container
+ * @returns {HTMLElement} Slides container element
+ */
+ getOrCreateSlidesContainer() {
+ let container = document.getElementById("slides-container");
+ if (!container) {
+ container = this.createElement('div', { id: 'slides-container' });
+ document.body.appendChild(container);
+ }
+ return container;
+ },
+
+ /**
+ * Formats genres into a readable string
+ * @param {Array} genresArray - Array of genre strings
+ * @returns {string} Formatted genres string
+ */
+ parseGenres(genresArray) {
+ if (Array.isArray(genresArray) && genresArray.length > 0) {
+ return genresArray.slice(0, 3).join(" 🔹 ");
+ }
+ return "No Genre Available";
+ },
+
+ /**
+ * Creates a loading indicator
+ * @returns {HTMLElement} Loading indicator element
+ */
+ createLoadingIndicator() {
+ const loadingIndicator = this.createElement('div', {
+ className: 'slide-loading-indicator',
+ innerHTML: `
+
+ `
+ });
+ return loadingIndicator;
}
};
-initLoadingScreen();
-const observer = new MutationObserver((mutations) => {
- for (let mutation of mutations) {
- if (window.location.hash.includes("#/home.html")) {
- let container = document.getElementById("slides-container");
-
- if (!container) {
- container = document.createElement("div");
- container.id = "slides-container";
- document.body.appendChild(container);
- slidesInit();
+/**
+ * API utilities for fetching data from Jellyfin server
+ */
+const ApiUtils = {
+ /**
+ * Fetches details for a specific item by ID
+ * @param {string} itemId - Item ID
+ * @returns {Promise