Files
MediaFusion/resources/js/config_script.js
T

1103 lines
44 KiB
JavaScript

// ---- Variables ----
const oAuthBtn = document.getElementById('oauth_btn');
let currentAuthorizationToken = null;
let mdbListUI;
const servicesRequiringCredentials = ['pikpak',];
const servicesRequiringUrl = ['stremthru'];
const servicesNotNeedingDebridProxy = ['stremthru'];
const providerSignupLinks = {
pikpak: 'https://mypikpak.com/drive/activity/invited?invitation-code=52875535',
seedr: 'https://www.seedr.cc/?r=2726511',
offcloud: 'https://offcloud.com/?=9932cd9f',
realdebrid: ['http://real-debrid.com/?id=9490816', 'http://real-debrid.com/?id=3351376'],
debridlink: 'https://debrid-link.com/id/kHgZs',
alldebrid: 'https://alldebrid.com/?uid=3ndha&lang=en',
torbox: ['https://torbox.app/subscription?referral=339b923e-fb23-40e7-8031-4af39c212e3c', 'https://torbox.app/subscription?referral=e2a28977-99ed-43cd-ba2c-e90dc398c49c', 'https://torbox.app/subscription?referral=38f1c266-8a6c-40b2-a6d2-2148e77dafc9'],
easydebrid: 'https://paradise-cloud.com/products/easydebrid',
premiumize: 'https://www.premiumize.me',
qbittorrent: 'https://github.com/mhdzumair/MediaFusion/tree/main/streaming_providers/qbittorrent#qbittorrent-webdav-setup-options-with-mediafusion',
stremthru: 'https://github.com/MunifTanjim/stremthru?tab=readme-ov-file#configuration',
};
const providerTokenTooltip = {
'*': `Enter Encoded Token previously generated or Click 'Authorize' to generate a new token or Provide your Private Token.`,
stremthru: `Enter Credential ('store_name:store_token' or base64 encoded basic token from 'STREMTHRU_PROXY_AUTH' config)`,
};
// ---- OAuth-related Functions ----
function generateUniqueToken() {
return Date.now().toString() + Math.random().toString();
}
async function initiateOAuthFlow(getDeviceCodeUrl, authorizeUrl) {
const provider = document.getElementById('provider_service').value;
currentAuthorizationToken = generateUniqueToken();
setOAuthBtnTextContent(provider);
try {
const response = await fetch(getDeviceCodeUrl);
const data = await response.json();
if (data.device_code) {
document.getElementById('device_code_display').textContent = data.user_code;
if (data.verification_url) {
const verificationLinkElement = document.getElementById('verification_link');
verificationLinkElement.href = data.verification_url;
verificationLinkElement.textContent = data.verification_url;
}
setElementDisplay('device_code_section', 'block');
checkAuthorization(data.device_code, authorizeUrl);
} else {
// Reset the button's text if there's an error or if the device code isn't returned
setOAuthBtnTextContent(provider);
}
} catch (error) {
console.error('Error fetching device code:', error);
alert('An error occurred. Please try again.');
setOAuthBtnTextContent(provider)
}
}
function checkAuthorization(deviceCode, authorizeUrl) {
const thisAuthorizationToken = currentAuthorizationToken;
setTimeout(async function () {
// Exit if the token has changed
if (thisAuthorizationToken !== currentAuthorizationToken) {
return;
}
try {
const response = await fetch(authorizeUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({device_code: deviceCode})
});
const data = await response.json();
if (data.token) {
const tokenInput = document.getElementById('provider_token');
tokenInput.value = data.token;
oAuthBtn.disabled = false;
tokenInput.disabled = false;
setElementDisplay('oauth_section', 'none');
setElementDisplay('device_code_section', 'none');
} else {
checkAuthorization(deviceCode, authorizeUrl);
}
} catch (error) {
console.error('Error checking authorization:', error);
oAuthBtn.disabled = false;
}
}, 5000);
}
// Function to update the file size output display
function updateSizeOutput() {
const slider = document.getElementById('max_size_slider');
const output = document.getElementById('max_size_output');
const value = slider.value;
const maxSize = slider.max;
output.textContent = value === maxSize ? 'Unlimited' : formatBytes(value);
updateSliderTrack(slider);
}
// ---- Helper Functions ----
function showNotification(message, type = 'info') {
toastr.options = {
closeButton: true,
newestOnTop: true,
progressBar: true,
positionClass: "toast-top-center",
preventDuplicates: true,
onclick: null,
showDuration: "300",
hideDuration: "1000",
timeOut: "5000",
extendedTimeOut: "1000",
showEasing: "swing",
hideEasing: "linear",
showMethod: "fadeIn",
hideMethod: "fadeOut",
};
toastr[type](message);
}
function setElementDisplay(elementId, displayStatus) {
document.getElementById(elementId).style.display = displayStatus;
}
function changeTooltipContent(elementId, text) {
document.getElementById(elementId).dataset.bsOriginalTitle = text;
}
function validateUrl(url) {
// This regex supports domain names, IPv4, and IPv6 addresses
const urlPattern = /^(https?:\/\/)?(([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\])(:?\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/i;
return urlPattern.test(url);
}
// Function to format bytes into a human-readable format
function formatBytes(bytes, decimals = 2) {
if (bytes === "0") return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// Function to update the slider track based on the current value
function updateSliderTrack(slider) {
const percentage = (slider.value / slider.max) * 100;
slider.style.background = `linear-gradient(to right, #4a47a3 ${percentage}%, #ddd ${percentage}%, #ddd 100%)`;
}
function setOAuthBtnTextContent(provider) {
if (currentAuthorizationToken) {
oAuthBtn.textContent = 'Authorizing...';
} else {
oAuthBtn.textContent = 'Authorize with ' + provider.charAt(0).toUpperCase() + provider.slice(1);
}
}
function adjustOAuthSectionDisplay() {
const provider = document.getElementById('provider_service').value;
const providerToken = document.getElementById('provider_token').value;
setOAuthBtnTextContent(provider)
if ((provider === 'seedr' || provider === 'realdebrid' || provider === 'debridlink' || provider === "premiumize") && !providerToken) {
setElementDisplay('oauth_section', 'block');
} else {
setElementDisplay('oauth_section', 'none');
}
}
function updateProviderFields(isChangeEvent = false) {
const provider = document.getElementById('provider_service').value;
const serviceUrlInput = document.getElementById('service_url');
const tokenInput = document.getElementById('provider_token');
const watchlistLabel = document.getElementById('watchlist_label');
if (servicesRequiringUrl.includes(provider)) {
setElementDisplay('service_url_section', 'block');
} else {
setElementDisplay('service_url_section', 'none');
}
if (servicesNotNeedingDebridProxy.includes(provider)) {
setElementDisplay('proxy_debrid_streams_section', 'none');
} else {
setElementDisplay('proxy_debrid_streams_section', 'block');
}
if (provider in providerSignupLinks) {
// if providerSignupLinks[provider] is an array, randomly select one of the links
if (Array.isArray(providerSignupLinks[provider])) {
document.getElementById('signup_link').href = providerSignupLinks[provider][Math.floor(Math.random() * providerSignupLinks[provider].length)];
} else {
document.getElementById('signup_link').href = providerSignupLinks[provider];
}
setElementDisplay('signup_section', 'block');
} else {
setElementDisplay('signup_section', 'none');
}
// Toggle visibility of credentials and token input based on provider
setElementDisplay('stremthru_config', provider === 'stremthru' ? 'block' : 'none');
if (provider) {
if (servicesRequiringCredentials.includes(provider)) {
setElementDisplay('credentials', 'block');
setElementDisplay('token_input', 'none');
setElementDisplay('qbittorrent_config', 'none');
} else if (provider === 'qbittorrent') {
setElementDisplay('qbittorrent_config', 'block');
setElementDisplay('credentials', 'none');
setElementDisplay('token_input', 'none');
} else {
setElementDisplay('credentials', 'none');
setElementDisplay('token_input', 'block');
changeTooltipContent('provider_token_tooltip', providerTokenTooltip[provider] || providerTokenTooltip['*'])
setElementDisplay('qbittorrent_config', 'none');
}
setElementDisplay('streaming_provider_options', 'block');
watchlistLabel.textContent = `Enable ${provider.charAt(0).toUpperCase() + provider.slice(1)} Watchlist`;
} else {
setElementDisplay('credentials', 'none');
setElementDisplay('token_input', 'none');
setElementDisplay('streaming_provider_options', 'none');
setElementDisplay('qbittorrent_config', 'none');
}
// Reset the fields only if this is triggered by an onchange event
if (isChangeEvent) {
serviceUrlInput.value = '';
tokenInput.value = '';
tokenInput.disabled = false;
currentAuthorizationToken = null;
oAuthBtn.disabled = false;
setElementDisplay('device_code_section', 'none');
}
adjustOAuthSectionDisplay();
}
// Function to show loading widget
function showLoadingWidget(message = "Processing your configuration...") {
const loadingWidget = document.getElementById('loadingWidget');
const loadingMessage = document.getElementById('loadingMessage');
if (loadingMessage) {
loadingMessage.textContent = message;
}
if (loadingWidget) {
loadingWidget.style.display = 'flex';
// Prevent background scrolling while loading
document.body.style.overflow = 'hidden';
}
}
// Function to hide loading widget
function hideLoadingWidget() {
const loadingWidget = document.getElementById('loadingWidget');
if (loadingWidget) {
loadingWidget.style.display = 'none';
// Restore background scrolling
document.body.style.overflow = '';
}
}
// Function to get installation URL
async function getInstallationUrl(isRedirect = false) {
try {
showLoadingWidget();
const userData = getUserData();
const existingConfig = document.getElementById('existing_config').value;
let urlPrefix = window.location.protocol + "//";
if (isRedirect) {
urlPrefix = "stremio://";
}
if (!userData) {
hideLoadingWidget();
showNotification('Validation failed. Please check your input.', 'error');
return null;
}
const encryptUrl = '/encrypt-user-data' + (existingConfig ? `/${existingConfig}` : '');
const response = await fetch(encryptUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
const data = await response.json();
if (data.status === 'error') {
hideLoadingWidget();
showNotification(data.message, 'error');
return null;
}
if (data.detail) {
hideLoadingWidget();
showNotification(data.detail[0].msg, 'error');
return null;
}
if (!data.encrypted_str) {
hideLoadingWidget();
showNotification('An error occurred while encrypting user data', 'error');
return null;
}
const installationUrl = urlPrefix + window.location.host + "/" + data.encrypted_str + "/manifest.json";
hideLoadingWidget();
return installationUrl;
} catch (error) {
hideLoadingWidget();
showNotification('An error occurred while encrypting user data', 'error');
console.error('Error encrypting user data:', error);
return null;
}
}
function getUserData() {
let isValid = true;
const provider = document.getElementById('provider_service').value;
let streamingProviderData = {};
const validateInput = (elementId, condition) => {
const element = document.getElementById(elementId);
if (condition) {
element.classList.remove('is-invalid');
} else {
element.classList.add('is-invalid');
if (isValid) {
element.focus(); // Set focus on the first invalid input
}
isValid = false;
}
};
// Validate and collect streaming provider data
if (provider) {
if (servicesRequiringUrl.includes(provider)) {
const serviceUrl = document.getElementById('service_url').value.trim();
validateInput('service_url', validateUrl(serviceUrl));
streamingProviderData.url = serviceUrl;
}
if (provider === 'stremthru') {
streamingProviderData.stremthru_store_name = document.getElementById('stremthru_store_name').value.trim() || null;
}
if (servicesRequiringCredentials.includes(provider)) {
validateInput('email', document.getElementById('email').value);
validateInput('password', document.getElementById('password').value);
streamingProviderData.email = document.getElementById('email').value;
streamingProviderData.password = document.getElementById('password').value;
} else if (provider === 'qbittorrent') {
streamingProviderData.qbittorrent_config = {
qbittorrent_url: document.getElementById('qbittorrent_url').value,
qbittorrent_username: document.getElementById('qbittorrent_username').value,
qbittorrent_password: document.getElementById('qbittorrent_password').value,
seeding_time_limit: parseInt(document.getElementById('seeding_time_limit').value, 10),
seeding_ratio_limit: parseFloat(document.getElementById('seeding_ratio_limit').value),
play_video_after: parseInt(document.getElementById('play_video_after_download').value, 10),
category: document.getElementById('category').value,
webdav_url: document.getElementById('webdav_url').value,
webdav_username: document.getElementById('webdav_username').value,
webdav_password: document.getElementById('webdav_password').value,
webdav_downloads_path: document.getElementById('webdav_downloads_path').value,
};
// Validate qBittorrent-specific inputs
validateInput('qbittorrent_url', streamingProviderData.qbittorrent_config.qbittorrent_url);
validateInput('seeding_time_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_time_limit));
validateInput('seeding_ratio_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_ratio_limit));
validateInput('play_video_after_download', !isNaN(streamingProviderData.qbittorrent_config.play_video_after));
validateInput('category', streamingProviderData.qbittorrent_config.category);
} else {
validateInput('provider_token', document.getElementById('provider_token').value);
streamingProviderData.token = document.getElementById('provider_token').value;
}
streamingProviderData.service = provider;
streamingProviderData.enable_watchlist_catalogs = document.getElementById('enable_watchlist').checked;
if (document.getElementById('download_via_browser')) {
streamingProviderData.download_via_browser = document.getElementById('download_via_browser').checked;
}
streamingProviderData.only_show_cached_streams = document.getElementById('only_show_cached_streams').checked;
} else {
streamingProviderData = null;
}
const mediaflowEnabled = document.getElementById('enable_mediaflow').checked
let mediaflowConfig = null;
if (mediaflowEnabled) {
mediaflowConfig = {
proxy_url: document.getElementById('mediaflow_proxy_url').value,
api_password: document.getElementById('mediaflow_api_password').value,
public_ip: document.getElementById('mediaflow_public_ip').value,
proxy_live_streams: document.getElementById('proxy_live_streams').checked,
proxy_debrid_streams: document.getElementById('proxy_debrid_streams').checked
};
if (servicesNotNeedingDebridProxy.includes(provider)) {
mediaflowConfig.proxy_debrid_streams = false;
}
validateInput('mediaflow_proxy_url', mediaflowConfig.proxy_url.trim() !== '');
validateInput('mediaflow_api_password', mediaflowConfig.api_password.trim() !== '');
}
let rpdbConfig = null;
if (document.getElementById('enable_rpdb').checked) {
rpdbConfig = {
api_key: document.getElementById('rpdb_api_key').value,
};
validateInput('rpdb_api_key', rpdbConfig.api_key.trim() !== '');
}
let mdblistConfig = null;
if (document.getElementById('enable_mdblist').checked) {
mdblistConfig = {
api_key: document.getElementById('mdblist_api_key').value,
lists: mdbListUI.getSelectedListsData()
};
}
// Collect and validate other user data
const maxSizeSlider = document.getElementById('max_size_slider');
const maxSizeValue = maxSizeSlider.value;
const maxSize = maxSizeSlider.max;
const maxSizeBytes = maxSizeValue === maxSize ? 'inf' : maxSizeValue;
const maxStreamsPerResolution = document.getElementById('maxStreamsPerResolution').value;
validateInput('maxStreamsPerResolution', maxStreamsPerResolution && !isNaN(maxStreamsPerResolution) && maxStreamsPerResolution > 0);
let apiPassword = null;
// Check for API Password if authentication is required
if (document.getElementById('api_password')) {
validateInput('api_password', document.getElementById('api_password').value);
apiPassword = document.getElementById('api_password').value;
}
if (!isValid) {
return null; // Return null if validation fails
}
// Collect sorting options with their directions
const selectedSortingOptions = Array.from(
document.querySelectorAll('#streamSortOrder .form-check-input:checked')
).map(el => ({
key: el.value,
direction: document.getElementById(`direction_${el.value}`).value
}));
// Collect and return the rest of the user data
const torrentDisplayOption = document.querySelector('input[name="torrentDisplayOption"]:checked').value;
const showTorrentLanguageFlag = document.getElementById('showTorrentLanguageFlag').checked;
// Collect nudity filter data
let selectedNudityFilters = Array.from(document.querySelectorAll('input[name="nudity_filter"]:checked')).map(el => el.value);
if (selectedNudityFilters.length === 0) {
selectedNudityFilters = ['Disable'];
}
// Collect certification filter data
let selectedCertificationFilters = Array.from(document.querySelectorAll('input[name="certification_filter"]:checked')).map(el => el.value);
if (selectedCertificationFilters.length === 0) {
selectedCertificationFilters = ['Disable'];
}
// Collect language sorting order
const languageSorting = Array.from(document.querySelectorAll('input[name="selected_languages"]:checked')).map(el => el.value || null);
// Collect quality filter data
const selectedQualityFilters = Array.from(document.querySelectorAll('input[name="quality_filter"]:checked')).map(el => el.value);
return {
streaming_provider: streamingProviderData,
selected_catalogs: Array.from(document.querySelectorAll('input[name="selected_catalogs"]:checked')).map(el => el.value),
selected_resolutions: Array.from(document.querySelectorAll('input[name="selected_resolutions"]:checked')).map(el => el.value || null),
enable_catalogs: document.getElementById('enable_catalogs').checked,
enable_imdb_metadata: document.getElementById('enable_imdb_metadata').checked,
max_size: maxSizeBytes,
max_streams_per_resolution: maxStreamsPerResolution,
torrent_sorting_priority: selectedSortingOptions,
show_full_torrent_name: torrentDisplayOption === 'fullName',
show_language_country_flag: showTorrentLanguageFlag,
nudity_filter: selectedNudityFilters,
certification_filter: selectedCertificationFilters,
language_sorting: languageSorting,
quality_filter: selectedQualityFilters,
api_password: apiPassword,
mediaflow_config: mediaflowConfig,
rpdb_config: rpdbConfig,
live_search_streams: document.getElementById('liveSearchStreams').checked,
contribution_streams: document.getElementById('contributionStreams').checked,
mdblist_config: mdblistConfig,
};
}
// Function to display the installation URL in a textarea for manual copying
function displayFallbackUrl(url) {
const container = document.getElementById('fallbackUrlContainer');
const textarea = document.getElementById('fallbackUrl');
textarea.value = url;
container.style.display = 'block'; // Make the container visible
textarea.focus();
}
// Configuration Mode Handling
function setConfigMode(mode) {
// Update button states
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(mode + '_mode').classList.add('active');
// Update description
const description = document.querySelector('.mode-description');
description.textContent = mode === 'pro'
? 'Pro Mode: Access to all advanced configuration options'
: 'Newbie Mode: Quick setup with essential options for new users';
// Toggle visibility of pro sections
const proSections = document.querySelectorAll('.pro-mode-section');
proSections.forEach(section => {
section.style.display = mode === 'pro' ? 'block' : 'none';
});
// Handle specific settings visibility
const streamingSection = document.querySelector('.streaming-preferences');
if (streamingSection) {
const advancedOptions = streamingSection.querySelectorAll('.advanced-option');
advancedOptions.forEach(option => {
option.style.display = mode === 'pro' ? 'block' : 'none';
});
}
// Save preference with error handling
try {
localStorage.setItem('configMode', mode);
} catch (e) {
console.warn('Failed to save config mode preference:', e);
}
}
function setupPasswordToggle(passwordInputId, toggleButtonId, toggleIconId) {
document.getElementById(toggleButtonId).addEventListener('click', function (_) {
const passwordInput = document.getElementById(passwordInputId);
const passwordIcon = document.getElementById(toggleIconId);
if (passwordInput.type === "password") {
passwordInput.type = "text";
passwordIcon.className = "bi bi-eye-slash";
} else {
passwordInput.type = "password";
passwordIcon.className = "bi bi-eye";
}
});
}
// Function to handle configured credential fields
function handleConfiguredFields(field, isConfigured = false) {
const inputField = document.getElementById(field);
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'btn btn-outline-secondary reset-config-btn';
resetBtn.innerHTML = '<i class="bi bi-arrow-counterclockwise"></i>';
resetBtn.title = 'Reset Configuration';
if (isConfigured) {
inputField.setAttribute('readonly', true);
inputField.classList.add('configured-field');
// Add reset button next to the field
if (!inputField.nextElementSibling?.classList.contains('reset-config-btn')) {
inputField.parentElement.appendChild(resetBtn);
}
// Handle reset button click
resetBtn.onclick = () => {
inputField.value = '';
inputField.removeAttribute('readonly');
inputField.classList.remove('configured-field');
resetBtn.remove();
};
}
}
// Function to initialize configured fields
function initConfiguredFields(configuredFields) {
const sensitiveFields = [
'provider_token',
'password',
'qbittorrent_password',
'webdav_password',
'mediaflow_api_password',
'rpdb_api_key'
];
sensitiveFields.forEach(field => {
if (configuredFields.includes(field)) {
handleConfiguredFields(field, true);
}
});
}
async function initiateKodiSetup() {
// Show modal to input Kodi code
const kodiCodeModal = new bootstrap.Modal(document.getElementById('kodiCodeModal'));
kodiCodeModal.show();
}
async function submitKodiCodeAndSetup() {
const kodiCode = document.getElementById('kodiCodeInput').value;
if (kodiCode && kodiCode.length === 6) {
const kodiCodeModal = bootstrap.Modal.getInstance(document.getElementById('kodiCodeModal'));
kodiCodeModal.hide();
await setupKodiAddon(kodiCode);
} else {
showNotification('Please enter a valid 6-digit code.', 'error');
}
}
async function setupKodiAddon(kodiCode) {
showLoadingWidget('Preparing Kodi setup...')
const installationUrl = await getInstallationUrl();
if (installationUrl) {
showLoadingWidget('Setting up Kodi addon...')
try {
const response = await fetch('/kodi/associate_manifest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: kodiCode,
manifest_url: installationUrl
}),
});
const data = await response.json();
if (response.ok) {
showNotification('Kodi addon setup successful!', 'success');
} else {
showNotification(`An error occurred while setting up the Kodi addon. ${data.detail}`, 'error');
}
} catch (error) {
console.error('Error setting up Kodi addon:', error);
showNotification('An error occurred while setting up the Kodi addon.', 'error');
} finally {
hideLoadingWidget();
}
}
}
/**
* Add select/deselect buttons and search input for a container
*
* @param {string} containerId - ID of the container element
* @param {string} sectionName - Name of the section for labels and placeholders
* @param {string} checkboxSelector - CSS selector for checkboxes in the container
*/
function addSelectDeselectSearch(containerId, sectionName, checkboxSelector) {
const container = document.getElementById(containerId);
if (!container) return;
// Create a controls container
const controlsContainer = document.createElement('div');
controlsContainer.className = 'mb-3';
// Create search input
const searchGroup = document.createElement('div');
searchGroup.className = 'input-group mb-2';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'form-control form-control-sm';
searchInput.placeholder = `Search ${sectionName}...`;
searchInput.id = `${containerId}-search`;
searchInput.setAttribute('autocomplete', 'off');
const searchClearBtn = document.createElement('button');
searchClearBtn.className = 'btn btn-outline-secondary btn-sm';
searchClearBtn.type = 'button';
searchClearBtn.innerHTML = '<i class="bi bi-x"></i>';
searchClearBtn.setAttribute('data-bs-toggle', 'tooltip');
searchClearBtn.setAttribute('data-bs-placement', 'top');
searchClearBtn.setAttribute('title', 'Clear Search');
searchGroup.appendChild(searchInput);
searchGroup.appendChild(searchClearBtn);
// Create button group
const buttonGroup = document.createElement('div');
buttonGroup.className = 'btn-group btn-group-sm w-100';
const selectAllBtn = document.createElement('button');
selectAllBtn.type = 'button';
selectAllBtn.className = 'btn btn-outline-primary';
selectAllBtn.innerHTML = '<i class="bi bi-check-all"></i> Select All';
const deselectAllBtn = document.createElement('button');
deselectAllBtn.type = 'button';
deselectAllBtn.className = 'btn btn-outline-secondary';
deselectAllBtn.innerHTML = '<i class="bi bi-x-lg"></i> Deselect All';
buttonGroup.appendChild(selectAllBtn);
buttonGroup.appendChild(deselectAllBtn);
// Add elements to controls container
controlsContainer.appendChild(searchGroup);
controlsContainer.appendChild(buttonGroup);
// Insert controls at the beginning of the container
container.parentNode.insertBefore(controlsContainer, container);
// Add event listeners
selectAllBtn.addEventListener('click', function() {
const visibleCheckboxes = getVisibleCheckboxes(container, checkboxSelector);
visibleCheckboxes.forEach(checkbox => {
checkbox.checked = true;
});
});
deselectAllBtn.addEventListener('click', function() {
const visibleCheckboxes = getVisibleCheckboxes(container, checkboxSelector);
visibleCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
});
searchInput.addEventListener('input', function() {
filterItems(container, this.value.toLowerCase());
});
searchClearBtn.addEventListener('click', function() {
searchInput.value = '';
filterItems(container, '');
});
}
/**
* Get all visible checkboxes in a container
*
* @param {HTMLElement} container - Container element
* @param {string} checkboxSelector - CSS selector for checkboxes
* @returns {HTMLElement[]} Array of visible checkbox elements
*/
function getVisibleCheckboxes(container, checkboxSelector) {
const allCheckboxes = container.querySelectorAll(checkboxSelector);
return Array.from(allCheckboxes).filter(checkbox => {
const item = getItemContainer(checkbox);
return item && !item.classList.contains('d-none');
});
}
/**
* Get the container element of a checkbox
*
* @param {HTMLElement} checkbox - Checkbox element
* @returns {HTMLElement|null} Container element or null
*/
function getItemContainer(checkbox) {
// Navigate up to find the draggable container or column
let parent = checkbox.parentElement;
while (parent && !parent.classList.contains('draggable-catalog') &&
!parent.classList.contains('draggable-language') &&
!parent.classList.contains('col-12') &&
!parent.classList.contains('col-md-6') &&
!parent.classList.contains('col-lg-4')) {
parent = parent.parentElement;
}
return parent;
}
/**
* Filter items in a container based on search text
*
* @param {HTMLElement} container - Container element
* @param {string} searchText - Text to search for
*/
function filterItems(container, searchText) {
// Find all items (columns or containers)
const items = container.querySelectorAll('.draggable-catalog, .draggable-language, .col-12.col-md-6.col-lg-4');
if (searchText === '') {
// Show all items if search is empty
items.forEach(item => {
item.classList.remove('d-none');
});
} else {
// Show/hide items based on label text
items.forEach(item => {
const label = item.querySelector('label');
if (label && label.textContent.toLowerCase().includes(searchText)) {
item.classList.remove('d-none');
} else {
item.classList.add('d-none');
}
});
}
}
// ---- Event Listeners ----
document.getElementById('provider_token').addEventListener('input', function () {
adjustOAuthSectionDisplay();
});
document.getElementById('enable_mediaflow').addEventListener('change', function () {
setElementDisplay('mediaflow_config', this.checked ? 'block' : 'none');
});
document.getElementById('enable_rpdb').addEventListener('change', function () {
setElementDisplay('rpdb_config', this.checked ? 'block' : 'none');
});
document.getElementById('enable_mdblist').addEventListener('change', function() {
setElementDisplay('mdblist_config', this.checked ? 'block' : 'none');
});
// Event listener for the slider
document.getElementById('max_size_slider').addEventListener('input', updateSizeOutput);
oAuthBtn.addEventListener('click', async function () {
const provider = document.getElementById('provider_service').value;
oAuthBtn.disabled = true;
document.getElementById('provider_token').disabled = true;
if (provider === 'seedr') {
await initiateOAuthFlow('/streaming_provider/seedr/get-device-code', '/streaming_provider/seedr/authorize');
} else if (provider === 'realdebrid') {
await initiateOAuthFlow('/streaming_provider/realdebrid/get-device-code', '/streaming_provider/realdebrid/authorize');
} else if (provider === 'debridlink') {
await initiateOAuthFlow('/streaming_provider/debridlink/get-device-code', '/streaming_provider/debridlink/authorize')
} else if (provider === 'premiumize') {
return window.location.href = "/streaming_provider/premiumize/authorize";
}
});
document.getElementById('configForm').addEventListener('submit', async function (event) {
event.preventDefault();
showLoadingWidget('Preparing Stremio installation...');
const installationUrl = await getInstallationUrl(true);
if (installationUrl) {
window.location.href = installationUrl;
}
});
document.getElementById('shareBtn').addEventListener('click', async function (event) {
event.preventDefault();
showLoadingWidget('Preparing share URL...');
const manifestUrl = await getInstallationUrl();
if (manifestUrl) {
try {
await navigator.share({
title: 'MediaFusion Addon Manifest',
url: manifestUrl,
});
showNotification('Manifest URL shared successfully. Do not share this URL with unknown persons.', 'success');
} catch (error) {
displayFallbackUrl(manifestUrl);
showNotification('Unable to use Share API. URL is ready to be copied manually.', 'info');
}
}
});
document.getElementById('copyBtn').addEventListener('click', async function (event) {
event.preventDefault();
showLoadingWidget('Generating installation URL...');
const manifestUrl = await getInstallationUrl();
if (manifestUrl) {
try {
await navigator.clipboard.writeText(manifestUrl);
showNotification('Manifest URL copied to clipboard. Do not share this URL with unknown persons.', 'success');
} catch (error) {
displayFallbackUrl(manifestUrl);
showNotification('Unable to access clipboard. URL is ready to be copied manually.', 'info');
}
}
});
// ---- Initial Setup ----
document.addEventListener('DOMContentLoaded', function () {
mdbListUI = new MDBListUI();
updateProviderFields();
updateSizeOutput();
});
document.addEventListener('DOMContentLoaded', function () {
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
let tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
if (navigator.share) {
document.getElementById('shareBtn').style.display = 'block';
} else {
document.getElementById('copyBtn').style.display = 'block';
}
setupPasswordToggle('password', 'togglePassword', 'togglePasswordIcon');
// Check for API Password if authentication is required
if (document.getElementById('api_password')) {
setupPasswordToggle('api_password', 'toggleApiPassword', 'toggleApiPasswordIcon');
}
setupPasswordToggle('qbittorrent_password', 'toggleQbittorrentPassword', 'toggleQbittorrentPasswordIcon');
setupPasswordToggle('webdav_password', 'toggleWebdavPassword', 'toggleWebdavPasswordIcon');
setupPasswordToggle('mediaflow_api_password', 'toggleMediaFlowPassword', 'toggleMediaFlowPasswordIcon');
setupPasswordToggle('rpdb_api_key', 'toggleRPDBApiKey', 'toggleRPDBApiKeyIcon');
setupPasswordToggle('mdblist_api_key', 'toggleMDBListApiKey', 'toggleMDBListApiKeyIcon');
});
document.addEventListener('DOMContentLoaded', function () {
// Initialize sort direction toggles
const toggleButtons = document.querySelectorAll('.sort-direction-toggle');
toggleButtons.forEach(button => {
button.addEventListener('click', function () {
const sortId = this.dataset.sortId;
const directionInput = document.getElementById(`direction_${sortId}`);
const currentDirection = directionInput.value;
const newDirection = currentDirection === 'desc' ? 'asc' : 'desc';
// Update direction
directionInput.value = newDirection;
// Update button text and icon
const iconContainer = this.querySelector('.sort-text');
const tooltipTexts = this.attributes['sorting_info'].value.split('|');
const newTooltipText = tooltipTexts[newDirection === 'asc' ? 1 : 0];
this.title = newTooltipText;
this.dataset.bsOriginalTitle = newTooltipText;
iconContainer.innerHTML = `
<i class="bi bi-sort-${newDirection === 'asc' ? 'up-alt' : 'down'}"></i>
<span class="d-sm-inline ms-1">${newTooltipText}</span>
`;
});
});
// Enable/disable sort direction buttons based on checkbox state
const sortCheckboxes = document.querySelectorAll('[name="selected_sorting_options"]');
sortCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
const sortId = this.value;
const toggleButton = document.querySelector(`[data-sort-id="${sortId}"]`);
const sortItem = this.closest('.sort-item');
toggleButton.disabled = !this.checked;
if (this.checked) {
sortItem.classList.add('active');
} else {
sortItem.classList.remove('active');
}
});
});
});
document.addEventListener('DOMContentLoaded', function () {
// Initialize Sortable on the catalog container
new Sortable(document.getElementById('catalogs'), {
handle: '.draggable-catalog',
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
delay: 200,
delayOnTouchOnly: true,
filter: '.form-check-input',
preventOnFilter: false,
});
new Sortable(document.getElementById('streamSortOrder'), {
handle: '.sortable-list',
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
delay: 200,
delayOnTouchOnly: true,
filter: '.form-check-input',
preventOnFilter: false,
});
new Sortable(document.getElementById('languageSortOrder'), {
handle: '.draggable-language',
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
delay: 200,
delayOnTouchOnly: true,
filter: '.form-check-input',
preventOnFilter: false,
});
});
// Add event listeners for mode switching
document.addEventListener('DOMContentLoaded', function () {
let storedMode = 'newbie';
try {
const savedMode = localStorage.getItem('configMode');
if (savedMode) {
storedMode = savedMode;
}
} catch (e) {
console.warn('Failed to read config mode preference:', e);
}
setConfigMode(storedMode);
});
document.addEventListener('DOMContentLoaded', function () {
const kodiSetupBtn = document.getElementById('kodiSetupBtn');
if (kodiSetupBtn) {
kodiSetupBtn.addEventListener('click', initiateKodiSetup);
}
const submitKodiCode = document.getElementById('submitKodiCode');
if (submitKodiCode) {
submitKodiCode.addEventListener('click', submitKodiCodeAndSetup);
}
});
document.addEventListener('DOMContentLoaded', function () {
// Initialize configured fields if they exist
const configuredFields = JSON.parse(document.getElementById('configured_fields')?.value || '[]');
initConfiguredFields(configuredFields);
// Show or hide the language sort section based on the sorting options
document.querySelectorAll('input[name="selected_sorting_options"]').forEach(checkbox => {
checkbox.addEventListener('change', function () {
const languageSortSection = document.getElementById('languageSortSection');
if (document.querySelector('input[name="selected_sorting_options"][value="language"]').checked) {
languageSortSection.style.display = 'block';
} else {
languageSortSection.style.display = 'none';
}
});
});
// Initial check to show/hide the language sort section
if (document.querySelector('input[name="selected_sorting_options"][value="language"]').checked) {
document.getElementById('languageSortSection').style.display = 'block';
}
// Add UI elements for catalogs
addSelectDeselectSearch('catalogs', 'Catalogs', 'input[name="selected_catalogs"]');
// Add UI elements for languages
addSelectDeselectSearch('languageSortOrder', 'Languages', 'input[name="selected_languages"]');
// Initialize the parental guide checkboxes
const parentalGuideCheckboxes = document.querySelectorAll('.parental-guide-checkbox');
parentalGuideCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function () {
const category = this.name;
if (this.value === 'Disable' && this.checked) {
parentalGuideCheckboxes.forEach(cb => {
if (cb !== this && cb.name === category) {
cb.checked = false;
cb.parentNode.classList.add('disabled-checkbox');
}
});
} else if (this.value === 'Disable' && !this.checked) {
parentalGuideCheckboxes.forEach(cb => {
if (cb.name === category) {
cb.parentNode.classList.remove('disabled-checkbox');
}
});
} else if (this.checked) {
const disableCheckbox = document.querySelector(`input[name="${category}"][value="Disable"]`);
if (disableCheckbox) {
disableCheckbox.checked = false;
disableCheckbox.parentNode.classList.add('disabled-checkbox');
}
} else {
const anyChecked = Array.from(parentalGuideCheckboxes).some(cb => cb.checked && cb.name === category);
if (!anyChecked) {
const disableCheckbox = document.querySelector(`input[name="${category}"][value="Disable"]`);
if (disableCheckbox) {
disableCheckbox.parentNode.classList.remove('disabled-checkbox');
}
}
}
});
});
});