// Define spec options const SPEC_OPTIONS = { resolution: [ {value: '480p', label: '480p (SD)'}, {value: '576p', label: '576p (SD)'}, {value: '720p', label: '720p (HD)'}, {value: '1080p', label: '1080p (Full HD)'}, {value: '1440p', label: '1440p (2K)'}, {value: '2160p', label: '2160p (4K)'}, {value: '4K', label: '4K UHD'} ], quality: [ {value: 'BluRay', label: 'BluRay'}, {value: 'BluRay REMUX', label: 'BluRay REMUX'}, {value: 'BRRip', label: 'BRRip'}, {value: 'BDRip', label: 'BDRip'}, {value: 'WEB-DL', label: 'WEB-DL'}, {value: 'HDRip', label: 'HDRip'}, {value: 'DVDRip', label: 'DVDRip'}, {value: 'HDTV', label: 'HDTV'}, {value: 'CAM', label: 'CAM'}, {value: 'TeleSync', label: 'TeleSync'}, {value: 'SCR', label: 'SCR'} ], codec: [ {value: 'x264', label: 'x264'}, {value: 'x265', label: 'x265 (HEVC)'}, {value: 'h.264', label: 'H.264 (AVC)'}, {value: 'h.265', label: 'H.265 (HEVC)'}, {value: 'hevc', label: 'HEVC'}, {value: 'avc', label: 'AVC'}, {value: 'mpeg-2', label: 'MPEG-2'}, {value: 'mpeg-4', label: 'MPEG-4'}, {value: 'vp9', label: 'VP9'} ], audio: [ {value: 'AAC', label: 'AAC'}, {value: 'AC3', label: 'AC3 (Dolby Digital)'}, {value: 'DTS', label: 'DTS'}, {value: 'DTS-HD MA', label: 'DTS-HD MA'}, {value: 'TrueHD', label: 'Dolby TrueHD'}, {value: 'Atmos', label: 'Dolby Atmos'}, {value: 'DD+', label: 'DD+'}, {value: 'Dolby Digital Plus', label: 'Dolby Digital Plus'}, {value: 'DTS Lossless', label: 'DTS Lossless'} ], hdr: [ {value: 'DV', label: 'Dolby Vision'}, {value: 'HDR10+', label: 'HDR10+'}, {value: 'HDR', label: 'HDR'}, {value: 'SDR', label: 'SDR'} ] }; // ---- Helper Functions ---- function showNotification(message, type = 'info') { toastr.options = { closeButton: true, newestOnTop: true, progressBar: true, positionClass: "toast-top-center", preventDuplicates: true, onclick: null, showDuration: "1000", hideDuration: "3000", timeOut: "10000", extendedTimeOut: "1000", showEasing: "swing", hideEasing: "linear", showMethod: "fadeIn", hideMethod: "fadeOut", }; toastr[type](message); } function addGenre() { const genreValue = document.getElementById('genreInput').value.trim(); if (!genreValue) { showNotification('Please enter a valid genre.', 'error'); return; } const genreIndex = document.getElementById('genreInputs').children.length; const genreHtml = `
${genreValue}
`; document.getElementById('genreInputs').insertAdjacentHTML('beforeend', genreHtml); document.getElementById('genreInput').value = ''; // Clear input for next genre } function removeGenre(genreId) { document.getElementById(genreId).remove(); } function addStreamInput() { const streamIndex = document.getElementById('streamInputs').children.length; // Adjusted HTML to include 'Optional' guidance in labels const streamInputHtml = `
Stream #${streamIndex + 1}
`; document.getElementById('streamInputs').insertAdjacentHTML('beforeend', streamInputHtml); } // Function to toggle the disabled state of one input based on the value of another function toggleField(targetId, sourceValue) { const targetField = document.getElementById(targetId); targetField.disabled = !!sourceValue; } function removeStreamInput(streamId) { document.getElementById(streamId).remove(); } function setElementDisplay(elementId, displayStatus) { document.getElementById(elementId).style.display = displayStatus; } function resetButton(submitBtn, loadingSpinner) { submitBtn.disabled = false; loadingSpinner.style.display = 'none'; } document.querySelectorAll('input[name="m3uInputType"]').forEach(input => { input.addEventListener('change', function () { if (this.value === 'url') { setElementDisplay('m3uPlaylistUrlInput', ''); setElementDisplay('m3uPlaylistFileInput', 'none'); document.getElementById('m3uPlaylistFile').value = ''; // Clear file input } else { setElementDisplay('m3uPlaylistUrlInput', 'none'); setElementDisplay('m3uPlaylistFileInput', ''); document.getElementById('m3uPlaylistUrl').value = ''; // Clear URL input } }); }); function toggleSpiderSpecificFields() { const spiderName = document.getElementById('spiderName').value; const isTamil = spiderName === 'tamilmv' || spiderName === 'tamil_blasters'; const isOtherSpider = !isTamil; // Display the specific options for TamilMV and TamilBlasters setElementDisplay('tamilmvTamilblastersParams', isTamil ? 'block' : 'none'); setElementDisplay('scrapeAllOption', isOtherSpider ? 'block' : 'none'); // Initially set to page scraping if (isTamil) { document.querySelector('input[name="mode"][value="page_scraping"]').checked = true; toggleModeSpecificFields(); } // Reset fields when changing mode document.getElementById('keyword').value = ''; document.getElementById('pages').value = '1'; document.getElementById('startPage').value = '1'; document.getElementById('totalPages').value = '1'; document.getElementById('scrape_all').checked = false; } 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 toggleModeSpecificFields() { const selectedMode = document.querySelector('input[name="mode"]:checked').value; const displayKeywordSearch = selectedMode === 'keyword_search'; const displayPageScraping = selectedMode === 'page_scraping'; // Display the appropriate input fields setElementDisplay('keywordSearchInput', displayKeywordSearch ? 'block' : 'none'); setElementDisplay('pageScrapingInput', displayPageScraping ? 'block' : 'none'); } function showConfirmationDialog(validationErrors, torrentData, infoHash) { return new Promise((resolve) => { // Remove any existing modal const existingModal = document.getElementById('confirmationModal'); if (existingModal) { existingModal.remove(); } const errorMessages = validationErrors.map(error => `
  • ${error.message}
  • `).join(''); const modalHtml = ` `; // Add modal to document document.body.insertAdjacentHTML('beforeend', modalHtml); const modalElement = document.getElementById('confirmationModal'); const modal = new bootstrap.Modal(modalElement); // Enable/disable confirm button based on checkbox document.getElementById('confirmGuidelines').addEventListener('change', (e) => { document.getElementById('confirmImport').disabled = !e.target.checked; }); // Handle confirmation button click document.getElementById('confirmImport').addEventListener('click', () => { if (document.getElementById('confirmGuidelines').checked) { modal.hide(); resolve(true); } }); // Handle modal hidden event modalElement.addEventListener('hidden.bs.modal', () => { modal.dispose(); modalElement.remove(); resolve(false); }); modal.show(); }); } function updateContentType() { const metaType = document.getElementById('metaType').value; // Hide all catalog sections first setElementDisplay('movieCatalogs', 'none'); setElementDisplay('seriesCatalogs', 'none'); setElementDisplay('sportsCatalogs', 'none'); // Show relevant catalog section based on content type if (metaType === 'movie') { setElementDisplay('movieCatalogs', 'block'); setElementDisplay('metaIdContainer', 'block'); } else if (metaType === 'series') { setElementDisplay('seriesCatalogs', 'block'); setElementDisplay('metaIdContainer', 'block'); } else if (metaType === 'sports') { setElementDisplay('sportsCatalogs', 'block'); setElementDisplay('metaIdContainer', 'none'); } } function parseSeasonsInput(input) { const seasons = []; const parts = input.split(','); for (const part of parts) { if (part.includes('-')) { const [start, end] = part.split('-').map(num => parseInt(num.trim())); for (let i = start; i <= end; i++) { seasons.push(i); } } else { const season = parseInt(part.trim()); if (!isNaN(season)) { seasons.push(season); } } } return seasons; } function showFileAnnotationModal(files) { const modal = document.getElementById('fileAnnotationModal'); const fileList = document.getElementById('fileAnnotationList'); fileList.innerHTML = ''; const isSportsContent = document.getElementById('metaType').value === 'sports'; // Sort files by filename files.sort((a, b) => { return a.filename.localeCompare(b.filename, undefined, { numeric: true, sensitivity: 'base' }); }); files.forEach((file, index) => { const fileRow = `
    ${file.filename}
    ${isSportsContent ? ` ` : ''}
    `; fileList.insertAdjacentHTML('beforeend', fileRow); }); files.forEach((file, index) => { // Initialize date picker for each file setupDateInput(`release-${index}`, false); }); const bsModal = new bootstrap.Modal(modal); bsModal.show(); return new Promise((resolve, reject) => { document.getElementById('confirmAnnotation').onclick = () => { const annotatedFiles = []; files.forEach((file, index) => { // Only include files that are marked for inclusion if (document.getElementById(`include-file-${index}`)?.checked) { const baseData = { ...file, season_number: parseInt(document.getElementById(`season-${index}`).value) || null, episode_number: parseInt(document.getElementById(`episode-${index}`).value) || null, }; if (isSportsContent) { const releaseDate = document.getElementById(`release-${index}`).value; if (releaseDate) { const dateParts = releaseDate.split('/'); const date = new Date(`${dateParts[1]}/${dateParts[0]}/${dateParts[2]}`); baseData.release_date = date.toISOString().split('T')[0] } annotatedFiles.push({ ...baseData, title: document.getElementById(`title-${index}`).value || null, overview: document.getElementById(`overview-${index}`).value || null, thumbnail: document.getElementById(`thumbnail-${index}`).value || null, }); } else { annotatedFiles.push(baseData); } } }); bsModal.hide(); resolve(annotatedFiles); }; modal.addEventListener('hidden.bs.modal', () => { reject(new Error('Annotation cancelled')); }, {once: true}); }); } // Function to apply episode numbering starting from a specific index function applyEpisodeNumberingFrom(startIndex) { const startEpisodeNumber = 1; const resetOnSeasonChange = true; // Get all included file rows const fileRows = Array.from(document.querySelectorAll('.card[id^="file-row-"]')); // Filter rows starting from the specified index const relevantFiles = fileRows.slice(fileRows.findIndex(row => row.id === `file-row-${startIndex}`)); let episodeCounter = startEpisodeNumber; let lastSeason = null; // Clear any previous highlights document.querySelectorAll('.card[id^="file-row-"]').forEach(row => { row.classList.remove('numbered-file'); }); // Apply episode numbers with visual feedback relevantFiles.forEach((fileRow, idx) => { const index = fileRow.id.split('-')[2]; // Skip excluded files if (!document.getElementById(`include-file-${index}`)?.checked) { return; } // Get the season input for this file const seasonInput = document.getElementById(`season-${index}`); const currentSeason = parseInt(seasonInput.value) || null; // Reset episode counter if season changes and reset option is enabled if (resetOnSeasonChange && lastSeason !== null && currentSeason !== null && currentSeason !== lastSeason) { episodeCounter = 1; } // Set the episode number document.getElementById(`episode-${index}`).value = episodeCounter++; // Store the current season for next iteration if (currentSeason !== null) { lastSeason = currentSeason; } // Add visual highlight with slight delay for visual effect setTimeout(() => { fileRow.classList.add('numbered-file'); // Remove highlight after 1.5 seconds setTimeout(() => { fileRow.classList.remove('numbered-file'); }, 1500); }, idx * 50); }); showNotification('Episode numbering applied successfully', 'success'); } // Function to apply season numbering starting from a specific index function applySeasonNumberingFrom(startIndex) { // Get all file rows const fileRows = Array.from(document.querySelectorAll('.card[id^="file-row-"]')); // Get the starting season number from the user const startSeasonNumber = parseInt(document.getElementById(`season-${startIndex}`).value) || parseInt(prompt("Enter starting season number:", "1")) || 1; // Filter rows starting from the specified index const relevantFiles = fileRows.slice(fileRows.findIndex(row => row.id === `file-row-${startIndex}`)); let seasonCounter = startSeasonNumber; // Clear any previous highlights document.querySelectorAll('.card[id^="file-row-"]').forEach(row => { row.classList.remove('numbered-file'); }); // Apply season numbers with visual feedback relevantFiles.forEach((fileRow, idx) => { const index = fileRow.id.split('-')[2]; // Skip excluded files if (!document.getElementById(`include-file-${index}`)?.checked) { return; } // Set the season number document.getElementById(`season-${index}`).value = seasonCounter; // Add visual highlight with slight delay for visual effect setTimeout(() => { fileRow.classList.add('numbered-file'); // Remove highlight after 1.5 seconds setTimeout(() => { fileRow.classList.remove('numbered-file'); }, 1500); }, idx * 50); }); showNotification('Season numbering applied successfully', 'success'); } // Function to toggle file inclusion function toggleFileInclusion(index) { const checkbox = document.getElementById(`include-file-${index}`); const inputsContainer = document.getElementById(`file-inputs-${index}`); const fileRow = document.getElementById(`file-row-${index}`); const episodeMetadata = document.getElementById(`episode-metadata-${index}`); if (checkbox.checked) { // File is included inputsContainer.style.opacity = '1'; inputsContainer.style.pointerEvents = 'auto'; fileRow.classList.remove('excluded-file'); // Enable all inputs in the container inputsContainer.querySelectorAll('input, textarea').forEach(input => { input.disabled = false; }); episodeMetadata.style.opacity = '1'; episodeMetadata.style.pointerEvents = 'auto'; episodeMetadata.querySelector('input, textarea').forEach(input => { input.disabled = false; }); } else { // File is excluded inputsContainer.style.opacity = '0.5'; inputsContainer.style.pointerEvents = 'none'; fileRow.classList.add('excluded-file'); // Disable all inputs in the container inputsContainer.querySelectorAll('input, textarea').forEach(input => { input.disabled = true; }); episodeMetadata.style.opacity = '0.5'; episodeMetadata.style.pointerEvents = 'none'; episodeMetadata.querySelector('input, textarea').forEach(input => { input.disabled = true; }); } } function toggleInput(disableId, input) { document.getElementById(disableId).disabled = !!input.value; } // Function to update form fields based on scraper selection function updateFormFields() { // Hide all sections initially setElementDisplay('quickImportParameters', 'none'); setElementDisplay('scrapyParameters', 'none'); setElementDisplay('tvMetadataInput', 'none'); setElementDisplay('m3uPlaylistInput', 'none'); setElementDisplay("imdbDataParameters", "none"); setElementDisplay("torrentUploadParameters", "none"); setElementDisplay("apiPasswordContainer", "none"); setElementDisplay('blockTorrentParameters', 'none'); setElementDisplay('migrationParameters', 'none'); setElementDisplay('updateImagesParameters', 'none'); // Get the selected scraper type const scraperType = document.getElementById('scraperSelect').value; let authRequired = document.getElementById('apiPasswordEnabled').value === "true"; // Show the relevant section based on the selected scraper type switch (scraperType) { case 'quick_import': setElementDisplay('quickImportParameters', 'block'); authRequired = false; break; case 'add_torrent': setElementDisplay("torrentUploadParameters", "block"); authRequired = false; updateContentType(); break; case 'scrapy': // Show Scrapy-specific parameters setElementDisplay('scrapyParameters', 'block'); toggleSpiderSpecificFields(); authRequired = false; break; case 'add_tv_metadata': // Show TV Metadata input form setElementDisplay('tvMetadataInput', 'block'); // Ensure a stream input is displayed initially if (document.getElementById('streamInputs').children.length === 0) { addStreamInput(); } break; case 'add_m3u_playlist': setElementDisplay('m3uPlaylistInput', 'block'); break; case 'update_imdb_data': setElementDisplay("imdbDataParameters", "block"); authRequired = false; break; case 'block_torrent': setElementDisplay("blockTorrentParameters", "block"); break; case 'migrate_id': setElementDisplay('migrationParameters', 'block'); authRequired = false; break; case 'update_images': setElementDisplay('updateImagesParameters', 'block'); authRequired = false; break; } // Setup password toggle if the API password field is displayed if (authRequired) { setElementDisplay("apiPasswordContainer", "block"); setupPasswordToggle('api_password', 'toggleApiPassword', 'toggleApiPasswordIcon'); } } function handleInitialSetup() { const urlParams = new URLSearchParams(window.location.search); const action = urlParams.get('action'); // Set initial scraper type based on action if (action) document.getElementById('scraperSelect').value = action; // Update form fields based on initial selection updateFormFields(); } function setupDateInput(inputId, defaultToToday = false, initialDate = null) { const fp = flatpickr(`#${inputId}`, { dateFormat: "d/m/Y", altInput: true, altFormat: "d/m/Y", allowInput: true, defaultHour: 12, maxDate: "today", locale: { firstDayOfWeek: 1 }, theme: "dark", defaultDate: initialDate // Set initial date if provided }); if (defaultToToday && !initialDate) { const today = new Date(); fp.setDate(today); } } function constructTvMetadata() { // Basic TV Metadata collection let tvMetaData = { title: document.getElementById('title').value.trim(), poster: document.getElementById('poster').value.trim(), background: document.getElementById('background').value.trim(), country: document.getElementById('country').value.trim(), tv_language: document.getElementById('language').value.trim(), logo: document.getElementById('logo').value.trim(), genres: [], streams: [] }; // Collecting Genres document.querySelectorAll('#genreInputs div span').forEach(genre => { tvMetaData.genres.push(genre.textContent); }); // Validating and Collecting Streams const streamContainers = document.querySelectorAll('#streamInputs .card'); streamContainers.forEach((container, index) => { console.log(`streamName-${index}`); let stream = { name: document.getElementById(`streamName-${index}`).value.trim(), url: document.getElementById(`streamUrl-${index}`).value.trim(), ytId: document.getElementById(`streamYtId-${index}`).value.trim(), source: document.getElementById(`streamSource-${index}`).value.trim(), country: document.getElementById(`streamCountry-${index}`).value.trim(), drm_key_id: document.getElementById(`streamDrmKeyId-${index}`).value.trim(), drm_key: document.getElementById(`streamDrmKey-${index}`).value.trim(), behaviorHints: { proxyHeaders: document.getElementById(`streamProxyHeaders-${index}`) ? JSON.parse(document.getElementById(`streamProxyHeaders-${index}`).value || "{}") : {}, notWebReady: true } }; // Basic validation to ensure at least one of URL or YouTube ID is provided if (!stream.url && !stream.ytId) { throw new Error(`Stream #${index + 1}: Either a URL or a YouTube ID is required.`); } tvMetaData.streams.push(stream); }); // Additional validation logic as necessary if (tvMetaData.title === '') { throw new Error('Title is required.'); } // Assuming at least one stream is required if (tvMetaData.streams.length === 0) { throw new Error('At least one stream is required.'); } return tvMetaData; } async function handleQuickImport() { const metaType = document.getElementById('quickMetaType').value; const torrentFile = document.getElementById('quickTorrentFile').files[0]; const magnetLink = document.getElementById('quickMagnetLink').value.trim(); // Store the uploader in localStorage for persistence localStorage.setItem('quickUploaderName', document.getElementById('quickUploaderName').value); if (!torrentFile && !magnetLink) { showNotification('Please provide either a torrent file or magnet link', 'error'); return; } const formData = new FormData(); formData.append('meta_type', metaType); if (torrentFile) { formData.append('torrent_file', torrentFile); } else { formData.append('magnet_link', magnetLink); } try { setElementDisplay('analysisLoading', 'block'); setElementDisplay('matchResults', 'none'); const response = await fetch('/scraper/analyze_torrent', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { if (data.matches && data.matches.length > 0) { displayMatchResults(data.matches, data.torrent_data); } else { showNotification('No matches found. Try manual import.', 'warning'); switchToManualImport(data.torrent_data); } } else { showNotification(data.detail || 'Failed to analyze torrent', 'error'); } } catch (error) { showNotification('Error analyzing torrent: ' + error.message, 'error'); } finally { setElementDisplay('analysisLoading', 'none'); } } function displayMatchResults(matches, torrentData) { const container = document.getElementById('matchResultsContent'); container.innerHTML = ''; if (matches && matches.length > 0) { matches.forEach(match => { const safeMatchData = btoa(encodeURIComponent(JSON.stringify(match))); const safeTorrentData = btoa(encodeURIComponent(JSON.stringify(torrentData))); // Format runtime nicely const runtime = match.runtime ? match.runtime.replace('min', '').trim() + ' minutes' : 'N/A'; // Format rating with stars const rating = match.imdb_rating ? ` ${match.imdb_rating}/10` : 'No rating'; // Handle multiple AKA titles const akaTitles = match.aka_titles && match.aka_titles.length > 0 ? `
    Also known as: ${match.aka_titles.join(' • ')}
    ` : ''; const matchHtml = `
    ${match.title}
    ${match.title} (${match.year})
    ${akaTitles}
    ${rating}
    ${match.type.charAt(0).toUpperCase() + match.type.slice(1)}
    ${runtime}
    ${match.imdb_id}

    ${match.description || 'No description available.'}

    ${match.genres.map(genre => `${genre}` ).join('')}
    ${match.countries.join(', ') || 'N/A'}
    ${match.languages.join(', ') || 'N/A'}
    ${match.stars && match.stars.length > 0 ? `
    ${match.stars.join(', ')}
    ` : ''}
    `; container.insertAdjacentHTML('beforeend', matchHtml); }); setElementDisplay('matchResults', 'block'); } else { showNotification('No matches found. Try manual import.', 'warning'); switchToManualImport(torrentData); } } function selectMatchFromData(button) { try { const matchData = button.getAttribute('data-match'); const torrentData = button.getAttribute('data-torrent'); // Decode using modern approach const match = JSON.parse(decodeURIComponent(atob(matchData))); const torrent = JSON.parse(decodeURIComponent(atob(torrentData))); selectMatch(match, torrent); } catch (error) { console.error('Error parsing match data:', error); showNotification('Error processing match data', 'error'); } } function formatTechnicalSpec(value, type) { if (!value) return 'Not Set'; switch (type) { case 'resolution': return `${value}`; case 'audio' || 'languages' || 'hdr': return Array.isArray(value) ? value.join(', ') : value.split(',').join(', '); default: return value; } } function updateBasicTechnicalSpecs(torrentData = {}) { const specsContainer = document.querySelector('.technical-specs-basic'); if (!specsContainer) return; const specs = [ {icon: 'bi-display', label: 'Resolution', value: torrentData.resolution, type: 'resolution'}, {icon: 'bi-camera-reels', label: 'Quality', value: torrentData.quality, type: 'quality'}, {icon: 'bi-film', label: 'Video Codec', value: torrentData.codec, type: 'codec'}, {icon: 'bi-music-note-beamed', label: 'Audio', value: torrentData.audio, type: 'audio'}, {icon: 'bi-translate', label: 'Languages', value: torrentData.languages, type: 'languages'}, {icon: 'bi-tv', label: 'HDR', value: torrentData.hdr, type: 'hdr'}, ]; specsContainer.innerHTML = specs.map(spec => `
    ${spec.label}
    ${formatTechnicalSpec(spec.value, spec.type)}
    `).join(''); // Add click handlers to each spec item specsContainer.querySelectorAll('.spec-item').forEach(item => { item.addEventListener('click', function () { const specType = this.dataset.specType; const modal = new bootstrap.Modal(document.getElementById('specSelectionModal')); const modalTitle = document.getElementById('specModalTitle'); const singleSelection = document.getElementById('singleSpecSelection'); const multipleSelection = document.getElementById('multipleSpecSelection'); // Determine if this is a multi-select specification const isMultiSelect = ['audio', 'hdr', 'languages'].includes(specType); // Update modal title modalTitle.textContent = `Select ${this.querySelector('.spec-label').textContent}`; // Show appropriate selection interface singleSelection.style.display = isMultiSelect ? 'none' : 'block'; multipleSelection.style.display = isMultiSelect ? 'block' : 'none'; // Get current value const currentValue = this.querySelector('.spec-value').textContent; // Populate options based on spec type if (isMultiSelect) { populateMultiSelect(specType, currentValue); } else { populateSingleSelect(specType, currentValue); } // Set up confirmation handler document.getElementById('confirmSpecSelection').onclick = () => { let selectedValue; if (isMultiSelect) { selectedValue = Array.from(document.querySelectorAll('.spec-checkboxes input:checked')) .map(cb => cb.value) .join(', '); } else { selectedValue = document.getElementById('specSelectSingle').value; } if (selectedValue) { // Update the spec display updateSpecField(specType, selectedValue); } modal.hide(); }; modal.show(); }); }); } // Update populateSingleSelect to use new options format function populateSingleSelect(specType, currentValue) { const select = document.getElementById('specSelectSingle'); select.innerHTML = ''; const options = getOptionsForSpecType(specType); options.forEach(option => { const opt = document.createElement('option'); opt.value = option.value; opt.textContent = option.label; opt.selected = currentValue === option.value; select.appendChild(opt); }); } // Function to enhance the spec selection modal for languages with search function enhanceLanguageSelection() { // Add a search field to the spec selection modal for languages const multipleSpecSelection = document.getElementById('multipleSpecSelection'); if (!multipleSpecSelection) return; // Create the search container if it doesn't exist if (!document.getElementById('languageSearchContainer')) { const searchHTML = ` `; multipleSpecSelection.insertAdjacentHTML('afterbegin', searchHTML); // Add event listener for the search input document.getElementById('languageSearch').addEventListener('input', function() { const searchValue = this.value.toLowerCase(); document.querySelectorAll('.spec-checkboxes .form-check').forEach(item => { const label = item.querySelector('label').textContent.toLowerCase(); if (label.includes(searchValue)) { item.style.display = 'block'; } else { item.style.display = 'none'; } }); }); // Add event listeners for select all and clear all buttons document.getElementById('selectAllLanguages').addEventListener('click', function() { const visibleCheckboxes = Array.from(document.querySelectorAll('.spec-checkboxes .form-check')) .filter(item => item.style.display !== 'none') .map(item => item.querySelector('input[type="checkbox"]')); visibleCheckboxes.forEach(checkbox => checkbox.checked = true); }); document.getElementById('clearAllLanguages').addEventListener('click', function() { const visibleCheckboxes = Array.from(document.querySelectorAll('.spec-checkboxes .form-check')) .filter(item => item.style.display !== 'none') .map(item => item.querySelector('input[type="checkbox"]')); visibleCheckboxes.forEach(checkbox => checkbox.checked = false); }); } } // Update populateMultiSelect to use new options format function populateMultiSelect(specType, currentValue) { const container = document.querySelector('.spec-checkboxes'); container.innerHTML = ''; const currentValues = currentValue.split(', ').map(v => v.trim()); const options = getOptionsForSpecType(specType); // Sort options alphabetically for better organization options.sort((a, b) => a.label.localeCompare(b.label)); options.forEach(option => { const div = document.createElement('div'); div.className = 'form-check'; div.innerHTML = ` `; container.appendChild(div); }); // Show search for languages, hide for others const searchContainer = document.getElementById('languageSearchContainer'); if (searchContainer) { searchContainer.style.display = specType === 'languages' ? 'block' : 'none'; // Clear search field when opening the modal if (specType === 'languages') { document.getElementById('languageSearch').value = ''; // Show all options initially document.querySelectorAll('.spec-checkboxes .form-check').forEach(item => { item.style.display = 'block'; }); } } } // Helper function to get current spec values function getSpecValues() { return { resolution: document.getElementById('resolutionSpec')?.textContent.trim(), quality: document.getElementById('qualitySpec')?.textContent.trim(), codec: document.getElementById('codecSpec')?.textContent.trim(), audio: document.getElementById('audioSpec')?.textContent.trim(), languages: document.getElementById('languagesSpec')?.textContent.trim(), hdr: document.getElementById('hdrSpec')?.textContent.trim() }; } // Helper function to get options for a spec type function getOptionsForSpecType(specType) { if (specType === 'languages') { // Get languages from hidden input try { const supportedLanguages = JSON.parse(document.getElementById('supportedLanguages').value); return supportedLanguages.map(lang => ({value: lang, label: lang})); } catch (e) { console.error('Error parsing supported languages:', e); return []; } } return SPEC_OPTIONS[specType] || []; } function switchToQuickImport() { document.getElementById('scraperSelect').value = 'quick_import'; updateFormFields(); formUtils.resetForm(); hideSuccessAlert(); window.scrollTo(0, 0); } // Function to show success message with Import New button function showSuccessWithImportNew(message) { const alert = document.getElementById('successAlert'); const messageElement = document.getElementById('successMessage'); // If alert doesn't exist, use notification if (!alert) { showNotification(message, 'success'); return; } messageElement.textContent = message; alert.style.display = 'block'; alert.classList.add('show'); // Scroll to the top to make the alert visible window.scrollTo(0, 0); } function hideSuccessAlert() { const alert = document.getElementById('successAlert'); if (alert) { alert.classList.remove('show'); alert.style.display = 'none'; // Clear the message const messageElement = document.getElementById('successMessage'); if (messageElement) { messageElement.textContent = ''; } } } function updateSpecField(fieldId, value) { const targetInput = document.getElementById(`${fieldId}Spec`); if (targetInput) { targetInput.innerHTML = formatTechnicalSpec(value, fieldId); } } function setupFieldChangeHandlers() { // Audio codecs handler const audioCodecsInputs = document.querySelectorAll('input[name="audioCodecs"]'); audioCodecsInputs.forEach(input => { input.addEventListener('change', () => { const selectedAudioCodecs = Array.from(audioCodecsInputs) .filter(input => input.checked) .map(input => input.value) .join(', '); updateSpecField('audio', selectedAudioCodecs); }); }); // HDR formats handler const hdrFormatsInputs = document.querySelectorAll('input[name="hdrFormats"]'); hdrFormatsInputs.forEach(input => { input.addEventListener('change', () => { const selectedHdrFormats = Array.from(hdrFormatsInputs) .filter(input => input.checked) .map(input => input.value) .join(', '); updateSpecField('hdr', selectedHdrFormats); }); }); // Languages handler const languageInputs = document.querySelectorAll('input[name="languages"]'); languageInputs.forEach(input => { input.addEventListener('change', () => { const selectedLanguages = Array.from(languageInputs) .filter(input => input.checked) .map(input => input.value) .join(', '); updateSpecField('languages', selectedLanguages); }); }); } // Utility functions for form handling const formUtils = { resetForm(preserveFileAndMagnet = false) { // Store torrent file and magnet link if needed let storedFile, storedMagnet; if (preserveFileAndMagnet) { const quickFile = document.getElementById('quickTorrentFile'); const quickMagnet = document.getElementById('quickMagnetLink'); storedFile = quickFile.files[0]; storedMagnet = quickMagnet.value; } // Reset basic fields const basicFields = ['torrentImdbId', 'metaType', 'title', 'poster', 'background', 'logo', 'resolution', 'quality', 'videoCodec', 'createdAt']; basicFields.forEach(fieldId => { const element = document.getElementById(fieldId); if (element) element.value = ''; }); // Reset checkboxes (audio, HDR, languages) ['audio-', 'hdr-', 'lang-'].forEach(prefix => { document.querySelectorAll(`input[id^="${prefix}"]`) .forEach(checkbox => checkbox.checked = false); }); // Reset catalog checkboxes for both movies and series ['movie', 'series'].forEach(type => { const container = document.getElementById(`${type}Catalogs`); if (container) { container.querySelectorAll('input[type="checkbox"]') .forEach(cb => cb.checked = false); } }); // Reset other options const addTitleCheckbox = document.getElementById('addTitleToPoster'); if (addTitleCheckbox) addTitleCheckbox.checked = false; // Restore torrent file and magnet link if needed if (preserveFileAndMagnet) { const quickFile = document.getElementById('quickTorrentFile'); const quickMagnet = document.getElementById('quickMagnetLink'); if (storedFile) { const dt = new DataTransfer(); dt.items.add(storedFile); quickFile.files = dt.files; } if (storedMagnet) { quickMagnet.value = storedMagnet; } } }, transferTorrentFile() { const quickFile = document.getElementById('quickTorrentFile'); const torrentFile = document.getElementById('torrentFile'); const magnetLink = document.getElementById('magnetLink'); const quickMagnet = document.getElementById('quickMagnetLink'); if (quickFile && quickFile.files[0]) { const dt = new DataTransfer(); dt.items.add(quickFile.files[0]); torrentFile.files = dt.files; } else if (quickMagnet) { magnetLink.value = quickMagnet.value.trim(); } } }; // Main functions for handling torrent imports function selectMatch(match, torrentData) { if (!match || !torrentData) return; // Reset all form fields while preserving torrent file/magnet formUtils.resetForm(true); // Set scraperSelect first to ensure proper form state document.getElementById('scraperSelect').value = 'add_torrent'; // Apply match data const matchFields = { 'imdb_id': 'torrentImdbId', 'type': 'metaType', 'title': 'title', 'poster': 'poster', 'background': 'background', 'logo': 'logo' }; Object.entries(matchFields).forEach(([matchKey, fieldId]) => { const element = document.getElementById(fieldId); if (element && match[matchKey]) { element.value = match[matchKey]; } }); // Set 'Add title to poster' option document.getElementById('addTitleToPoster').checked = match.is_add_title_to_poster || false; // Apply torrent specific data applyTorrentData(torrentData); updateFormFields(); showNotification('Match selected! Please review and confirm the details.', 'success'); } function switchToManualImport(torrentData) { // Reset all form fields while preserving torrent file/magnet formUtils.resetForm(true); document.getElementById('scraperSelect').value = 'add_torrent'; if (torrentData) { // Apply all torrent data applyTorrentData(torrentData); } updateFormFields(); } function applyTorrentData(torrentData) { if (!torrentData) return; // Update title if empty if (!document.getElementById('title').value && torrentData.title) { document.getElementById('title').value = torrentData.title; } // Set release date if (torrentData.created_at) { const date = new Date(torrentData.created_at); setupDateInput('createdAt', false, date.toLocaleDateString('en-GB')) } // Map torrent data fields to spec types const specMapping = { resolution: torrentData.resolution, quality: torrentData.quality, codec: torrentData.codec, audio: torrentData.audio, hdr: torrentData.hdr, languages: torrentData.languages }; // Helper function to format value based on spec type function formatSpecValue(type, value) { if (!value) return 'Not Set'; // For array-like values (could be array or comma-separated string) if (typeof value === 'string' && value.includes(',') || Array.isArray(value)) { const valueArray = Array.isArray(value) ? value : value.split(',').map(v => v.trim()); return valueArray.join(', '); } // For single values, validate against available options const options = getOptionsForSpecType(type); const option = options.find(opt => opt.value.toLowerCase() === value.toLowerCase()); return option ? option.value : value; } // Update basic specs display Object.entries(specMapping).forEach(([type, value]) => { const formattedValue = formatSpecValue(type, value); updateSpecField(type, formattedValue); }); // Update basic technical specs display updateBasicTechnicalSpecs({ resolution: specMapping.resolution, quality: specMapping.quality, codec: specMapping.codec, audio: specMapping.audio, hdr: specMapping.hdr, languages: specMapping.languages }); // Handle content type specific settings document.getElementById('metaType').value = torrentData.type; if (torrentData.type === 'movie' || torrentData.type === 'series') { if (torrentData.catalog) { const catalogs = Array.isArray(torrentData.catalog) ? torrentData.catalog : torrentData.catalog.split(',').map(c => c.trim()); const container = document.getElementById(`${torrentData.type}Catalogs`); if (container) { catalogs.forEach(catalog => { const checkbox = container.querySelector(`input[value="${catalog}"]`); if (checkbox) checkbox.checked = true; }); } } } // Transfer torrent file or magnet link formUtils.transferTorrentFile(); // Transfer additional fields from quick import const torrentType = document.getElementById('quickTorrentType'); const uploaderName = document.getElementById('quickUploaderName'); if (torrentType && torrentType.value) { document.getElementById('torrentType').value = torrentType.value; } if (uploaderName && uploaderName.value) { document.getElementById('uploaderName').value = uploaderName.value; } else if (localStorage.getItem('quickUploaderName')) { document.getElementById('uploaderName').value = localStorage.getItem('quickUploaderName'); } // Update UI to reflect content type updateContentType(); } async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false, annotatedFiles = null) { let formData = new FormData(); const metaType = document.getElementById('metaType').value; const torrentType = document.getElementById('torrentType').value; const isSportsContent = metaType === 'sports'; // Get current spec values const specValues = getSpecValues(); // Add basic metadata to formData formData.append('meta_type', metaType); formData.append('torrent_type', torrentType); formData.append('uploader', document.getElementById('uploaderName').value.trim() || 'Anonymous'); const createdAt = document.getElementById('createdAt').value; if (createdAt) { const dateParts = createdAt.split('/'); const date = new Date(`${dateParts[1]}/${dateParts[0]}/${dateParts[2]}`); formData.append('created_at', date.toISOString().split('T')[0]) } else { showNotification("Release Date is required.", 'error'); resetButton(submitBtn, loadingSpinner); return; } // Add content metadata if (!isSportsContent && document.getElementById('torrentImdbId')?.value) { formData.append('meta_id', document.getElementById('torrentImdbId').value); } const title = document.getElementById('title').value; if (!title && isSportsContent) { showNotification('Title is required for sports content.', 'error'); resetButton(submitBtn, loadingSpinner); return; } if (title) formData.append('title', title); // Add optional metadata ['poster', 'background', 'logo'].forEach(field => { const value = document.getElementById(field)?.value; if (value) formData.append(field, value); }); // Add technical specifications Object.entries(specValues).forEach(([key, value]) => { if (value && value !== 'Not Set') { formData.append(key, value); } }); // Handle catalogs if (isSportsContent) { const sportsCatalog = document.getElementById('sportsCatalog').value; if (!sportsCatalog) { showNotification('Sports category is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } formData.append('catalogs', sportsCatalog); } else { const catalogInputs = document.querySelectorAll(`#${metaType}Catalogs input[name="catalogs"]:checked`); const catalogs = Array.from(catalogInputs).map(el => el.value); if (catalogs.length > 0) { formData.append('catalogs', catalogs.join(',')); } } // Handle add title to poster option formData.append('is_add_title_to_poster', document.getElementById('addTitleToPoster').checked.toString()); // Handle torrent file/magnet const magnetLink = document.getElementById('magnetLink').value; const torrentFile = document.getElementById('torrentFile').files[0]; if (!magnetLink && !torrentFile) { showNotification('Either Magnet Link or Torrent File is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } if (torrentType !== 'public' && !torrentFile) { showNotification(`Torrent File is required for ${torrentType} torrents.`, 'error'); resetButton(submitBtn, loadingSpinner); return; } if (magnetLink) { formData.append('magnet_link', magnetLink); } else { formData.append('torrent_file', torrentFile); } // Add force import and annotated files if present if (forceImport) formData.append('force_import', 'true'); if (annotatedFiles) formData.append('file_data', JSON.stringify(annotatedFiles)); // Submit the form try { const response = await fetch('/scraper/torrent', { method: 'POST', body: formData }); const data = await response.json(); if (data.status === 'needs_annotation') { try { const newAnnotatedFiles = await showFileAnnotationModal(data.files); await handleAddTorrent(submitBtn, loadingSpinner, forceImport, newAnnotatedFiles); return; } catch (annotationError) { console.error('Error annotating files:', annotationError); showNotification('File annotation was cancelled', 'warning'); } } else if (data.status === 'validation_failed' && !forceImport) { const shouldForceImport = await showConfirmationDialog( data.errors, data.torrent_data, data.info_hash ); if (shouldForceImport) { await handleAddTorrent(submitBtn, loadingSpinner, true, annotatedFiles); return; } } else if (data.detail) { showNotification(data.detail, 'error'); } else { showSuccessWithImportNew(data.status); } } catch (error) { console.error('Error submitting torrent:', error); showNotification(`Error submitting torrent: ${error.toString()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleAddTvMetadata(payload, submitBtn, loadingSpinner) { try { payload['tv_metadata'] = constructTvMetadata(); const response = await fetch('/scraper/add_tv_metadata', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error constructing TV Metadata:', error); showNotification(error.message, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleAddM3uPlaylist(apiPassword, submitBtn, loadingSpinner) { let formData = new FormData(); formData.append('scraper_type', 'add_m3u_playlist'); formData.append('api_password', apiPassword); formData.append('m3u_playlist_source', document.getElementById('m3uPlaylistSource').value); const inputType = document.querySelector('input[name="m3uInputType"]:checked').value; if (inputType === 'url') { const m3uUrl = document.getElementById('m3uPlaylistUrl').value.trim(); if (!m3uUrl) { showNotification('M3U Playlist URL is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } formData.append('m3u_playlist_url', m3uUrl); } else { const file = document.getElementById('m3uPlaylistFile').files[0]; if (!file) { showNotification('M3U Playlist file is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } formData.append('m3u_playlist_file', file); } try { const response = await fetch('/scraper/m3u_upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error submitting scraper form:', error); showNotification(`Error submitting scraper form. Error: ${error.stringify()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleUpdateImdbData(submitBtn, loadingSpinner) { const imdbId = document.getElementById('imdbId').value; const imdbType = document.getElementById('imdbType').value; try { const response = await fetch(`/scraper/imdb_data?meta_id=${imdbId}&media_type=${imdbType}`, { method: 'GET', headers: {'Content-Type': 'application/json'} }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error submitting scraper form:', error); showNotification(`Error submitting scraper form. Error: ${error.stringify()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleMigration(apiPassword, submitBtn, loadingSpinner) { const mediafusionId = document.getElementById('mediafusionId').value.trim(); const imdbId = document.getElementById('migrationImdbId').value.trim(); const mediaType = document.getElementById('mediaType').value; if (!mediafusionId || !imdbId) { showNotification('Both MediaFusion ID and IMDb ID are required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } try { const response = await fetch('/scraper/migrate_id', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mediafusion_id: mediafusionId, imdb_id: imdbId, media_type: mediaType, api_password: apiPassword }) }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error migrating ID:', error); showNotification(`Error migrating ID: ${error.toString()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleScrapyParameters(payload, submitBtn, loadingSpinner) { document.querySelectorAll('#scrapyParameters input, #scrapyParameters select').forEach(input => { if (!input.disabled && input.type !== 'radio' && input.type !== 'checkbox') { payload[input.name] = input.value; } else if (input.type === 'radio' && input.checked) { payload[input.name] = input.value; } else if (input.type === 'checkbox') { payload[input.name] = input.checked; } }); try { const response = await fetch('/scraper/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error submitting scraper form:', error); showNotification(`Error submitting scraper form. Error: ${error.stringify()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleBlockTorrent(apiPassword, submitBtn, loadingSpinner) { const infoHash = document.getElementById('blockTorrentInfoHash').value.trim(); if (!infoHash) { showNotification('Torrent Info Hash is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } // get the action value from drop down blockTorrentAction const action = document.getElementById('blockTorrentAction').value.trim(); try { const response = await fetch('/scraper/block_torrent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ info_hash: infoHash, action: action, api_password: apiPassword }) }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); } } catch (error) { console.error('Error blocking torrent:', error); showNotification(`Error blocking torrent. Error: ${error.toString()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } async function handleUpdateImages(apiPassword, submitBtn, loadingSpinner) { const metaId = document.getElementById('imageUpdateMetaId').value.trim(); const poster = document.getElementById('imageUpdatePoster').value.trim(); const background = document.getElementById('imageUpdateBackground').value.trim(); const logo = document.getElementById('imageUpdateLogo').value.trim(); // Validate required fields if (!metaId) { showNotification('Content ID is required.', 'error'); resetButton(submitBtn, loadingSpinner); return; } // Validate at least one image URL is provided if (!poster && !background && !logo) { showNotification('At least one image URL must be provided.', 'error'); resetButton(submitBtn, loadingSpinner); return; } const formData = new FormData(); formData.append('meta_id', metaId); formData.append('api_password', apiPassword); if (poster) formData.append('poster', poster); if (background) formData.append('background', background); if (logo) formData.append('logo', logo); try { const response = await fetch('/scraper/update_images', { method: 'POST', body: formData }); const data = await response.json(); if (data.detail) { showNotification(data.detail, 'error'); } else { showNotification(data.status, 'success'); // Clear form fields on success document.getElementById('imageUpdatePoster').value = ''; document.getElementById('imageUpdateBackground').value = ''; document.getElementById('imageUpdateLogo').value = ''; } } catch (error) { console.error('Error updating images:', error); showNotification(`Error updating images: ${error.toString()}`, 'error'); } finally { resetButton(submitBtn, loadingSpinner); } } // Main function async function submitScraperForm() { const apiPassword = document.getElementById('api_password').value; const scraperType = document.getElementById('scraperSelect').value; const submitBtn = document.getElementById('submitBtn'); const loadingSpinner = document.getElementById('loadingSpinner'); let payload = {scraper_type: scraperType, api_password: apiPassword}; // Disable button and show loading spinner submitBtn.disabled = true; loadingSpinner.style.display = 'inline-block'; // Call the appropriate handler based on scraper type switch (scraperType) { case 'quick_import': resetButton(submitBtn, loadingSpinner); await handleQuickImport(submitBtn, loadingSpinner); break; case 'add_torrent': await handleAddTorrent(submitBtn, loadingSpinner); break; case 'add_tv_metadata': await handleAddTvMetadata(payload, submitBtn, loadingSpinner); break; case 'add_m3u_playlist': await handleAddM3uPlaylist(apiPassword, submitBtn, loadingSpinner); break; case 'update_imdb_data': await handleUpdateImdbData(submitBtn, loadingSpinner); break; case 'block_torrent': await handleBlockTorrent(apiPassword, submitBtn, loadingSpinner); break; case 'migrate_id': await handleMigration(apiPassword, submitBtn, loadingSpinner); break; case 'update_images': await handleUpdateImages(apiPassword, submitBtn, loadingSpinner); break; default: await handleScrapyParameters(payload, submitBtn, loadingSpinner); break; } } // Initial update for form fields on page load document.addEventListener('DOMContentLoaded', function () { handleInitialSetup(); updateBasicTechnicalSpecs(); setupFieldChangeHandlers(); setupDateInput('createdAt', true); // Restore saved uploader const savedUploaderName = localStorage.getItem('quickUploaderName'); if (savedUploaderName) { const quickUploaderName = document.getElementById('quickUploaderName'); if (quickUploaderName) { quickUploaderName.value = savedUploaderName; } } // Enhance language selection enhanceLanguageSelection(); }); document.getElementById('spiderName').addEventListener('change', toggleSpiderSpecificFields);