mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Introduced mechanisms to handle preconfigured sensitive fields with masking and reset options in the UI. Extended User functionality to reuse existing encrypted configuration for sensitive fields when editing. Improved styling and user experience for managing credentials and secure configurations.
919 lines
37 KiB
JavaScript
919 lines
37 KiB
JavaScript
// ---- Variables ----
|
|
const oAuthBtn = document.getElementById('oauth_btn');
|
|
let currentAuthorizationToken = null;
|
|
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',
|
|
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',
|
|
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) {
|
|
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', validateUrl(mediaflowConfig.proxy_url));
|
|
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() !== '');
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Collect nudity filter data
|
|
const selectedNudityFilters = Array.from(document.querySelectorAll('input[name="nudity_filter"]:checked')).map(el => el.value);
|
|
|
|
// Collect certification filter data
|
|
const selectedCertificationFilters = Array.from(document.querySelectorAll('input[name="certification_filter"]:checked')).map(el => el.value);
|
|
|
|
// 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',
|
|
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,
|
|
};
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- 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');
|
|
});
|
|
|
|
|
|
// 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 () {
|
|
updateProviderFields();
|
|
updateSizeOutput();
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var 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');
|
|
});
|
|
|
|
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-none 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';
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|