// ==UserScript== // @name M3Unator - Web Directory Playlist Creator // @namespace https://github.com/hasanbeder/M3Unator // @version 1.0.2 // @description Create M3U/M3U8 playlists from directory listing pages. Automatically finds video and audio files in web server indexes. // @author Hasan Beder // @license GPL-3.0 // @match *://*/* // @grant GM_addStyle // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWMyZTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjIzIDcgMTYgMTIgMjMgMTcgMjMgNyIvPjxyZWN0IHg9IjEiIHk9IjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNCIgcng9IjIiIHJ5PSIyIi8+PC9zdmc+ // @homepageURL https://github.com/hasanbeder/M3Unator // @supportURL https://github.com/hasanbeder/M3Unator/issues // @downloadURL https://raw.githubusercontent.com/hasanbeder/M3Unator/main/M3Unator.user.js // @updateURL https://raw.githubusercontent.com/hasanbeder/M3Unator/main/M3Unator.meta.js // @run-at document-end // @noframes true // ==/UserScript== (function() { 'use strict'; if (!document.title.includes('Index of') && !document.querySelector('div#table-list')) { console.log('This page is not an Index page, M3Unator disabled.'); return; } function parseLiteSpeedDirectory() { const links = []; const rows = document.querySelectorAll('#table-content tr'); rows.forEach(row => { const linkElement = row.querySelector('a'); if (linkElement && !linkElement.textContent.includes('Parent Directory')) { const href = linkElement.getAttribute('href'); if (href) { links.push(new URL(href, window.location.href).href); } } }); return links; } // Add LiteSpeed support to the existing getDirectoryLinks function function getDirectoryLinks() { const links = []; // LiteSpeed directory listing if (document.querySelector('div#table-list')) { const rows = document.querySelectorAll('#table-content tr'); rows.forEach(row => { const linkElement = row.querySelector('a'); if (linkElement && !linkElement.textContent.includes('Parent Directory')) { const href = linkElement.getAttribute('href'); if (href) { links.push(new URL(href, window.location.href).href); } } }); return links; } // Apache/Nginx style directory listing const anchors = document.querySelectorAll('a'); anchors.forEach(anchor => { if (!anchor.textContent.includes('Parent Directory')) { const href = anchor.getAttribute('href'); if (href && !href.startsWith('?') && !href.startsWith('/')) { links.push(new URL(href, window.location.href).href); } } }); return links; } GM_addStyle(` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); [class^="M3Unator"] { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; } .M3Unator-title { font-weight: 700; letter-spacing: -0.02em; } .M3Unator-input-group label { font-weight: 500; letter-spacing: -0.01em; } .M3Unator-input { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 0.9375rem; letter-spacing: -0.01em; } .M3Unator-button { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-weight: 600; letter-spacing: -0.01em; } .M3Unator-control-btn { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-weight: 500; letter-spacing: -0.01em; } .M3Unator-log { font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.8125rem; letter-spacing: -0.01em; line-height: 1.5; } .M3Unator-log-counter { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-weight: 600; letter-spacing: -0.01em; } .M3Unator-container { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(8px); display: none; place-items: center; padding: 1rem; z-index: 9999; } .M3Unator-container[data-visible="true"] { display: grid; } .M3Unator-overlay { position: fixed; inset: 0; background: transparent; z-index: 9998; } body.modal-open { overflow: hidden; pointer-events: none; /* Prevent background clicks */ } body.modal-open .M3Unator-container, body.modal-open .M3Unator-popup { pointer-events: all; /* Allow clicks on modal content */ } .M3Unator-popup { background: #11111b; color: #cdd6f4; width: 100%; max-width: 480px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); overflow: hidden; animation: slideUp 0.3s ease; position: absolute; } .M3Unator-header { padding: 1.25rem 1.618rem; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: space-between; cursor: move; user-select: none; border-bottom: 1px solid #313244; } .M3Unator-title { display: flex; align-items: center; gap: 0.75rem; margin: 0; font-size: 1.25rem; font-weight: 600; line-height: 1; } .M3Unator-title svg { width: 24px; height: 24px; color: #f5c2e7; filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4)); flex-shrink: 0; display: flex; align-items: center; justify-content: center; margin-top: 1px; } .M3Unator-title span { display: flex; align-items: center; line-height: 24px; background: linear-gradient(90deg, #f5c2e7, #cba6f7, #89b4fa, #a6e3a1, #f5c2e7 ); background-size: 300% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: gradient 3s linear infinite; } .M3Unator-close { background: rgba(203, 166, 247, 0.1); border: none; color: #cba6f7; width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; cursor: pointer; transition: all 0.2s ease; } .M3Unator-close:hover { background: rgba(203, 166, 247, 0.2); transform: rotate(360deg); } .M3Unator-close svg { width: 18px; height: 18px; } .M3Unator-content { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem; } .M3Unator-input-group { margin-bottom: 0; } .M3Unator-input-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #bac2de; } .M3Unator-input { width: 100%; height: 42px; /* Same height as Create Playlist button */ padding: 0 12px; border: 1px solid #45475a; border-radius: 8px; background: #1e1e2e; color: #f5c2e7; font-size: 14px; transition: all 0.2s ease; box-sizing: border-box; } .M3Unator-input:focus { outline: none; border-color: #f5c2e7; box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1); } .M3Unator-input::placeholder { color: #6c7086; opacity: 1; } .M3Unator-toggle-container { position: relative; display: flex; align-items: center; justify-content: center; } .M3Unator-toggle-container input[type="checkbox"] { display: none; } .M3Unator-toggle-container span { width: 48px; height: 48px; background: #1e1e2e; border: 2px solid #45475a; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .M3Unator-toggle-container svg { width: 24px; height: 24px; opacity: 0.7; transition: all 0.3s ease; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .M3Unator-toggle-container input[type="checkbox"]:checked + span { background: rgba(203, 166, 247, 0.1); border-color: #cba6f7; box-shadow: 0 0 20px rgba(203, 166, 247, 0.2); } .M3Unator-toggle-container input[type="checkbox"]:checked + span svg { opacity: 1; color: #cba6f7; filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4)); } .M3Unator-toggle-container span:hover { background: #313244; transform: translateY(-2px); } .M3Unator-toggle-container span:active { transform: translateY(1px); } .M3Unator-toggle-container input[type="checkbox"]:checked + span:hover { background: rgba(203, 166, 247, 0.2); } .M3Unator-toggle-container span:active { transform: translateY(1px); } .M3Unator-toggle-container svg { width: 24px; height: 24px; opacity: 0.8; transition: all 0.2s ease; } .M3Unator-toggle-container input[type="checkbox"]:checked + span svg { opacity: 1; color: #cba6f7; } .M3Unator-toggle-group { display: flex; gap: 0.75rem; margin: 0.75rem 0; justify-content: center; background: rgba(30, 30, 46, 0.4); padding: 0.75rem; border-radius: 12px; backdrop-filter: blur(8px); } [title]:hover::after { content: attr(title); position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%); padding: 0.5rem 0.75rem; background: rgba(30, 30, 46, 0.95); color: #cdd6f4; font-size: 0.875rem; white-space: nowrap; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; border: 1px solid #313244; text-align: center; backdrop-filter: blur(8px); pointer-events: none; } .M3Unator-button { width: 100%; height: 42px; padding: 0 16px; border: none; border-radius: 8px; background: #f5c2e7; color: #1e1e2e; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } .M3Unator-button:hover { background: #f5c2e7; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(245, 194, 231, 0.2); } .M3Unator-button:active { transform: translateY(0); } .M3Unator-button:disabled { opacity: 0.5; cursor: not-allowed; } .M3Unator-launcher { position: fixed; top: 1rem; right: 1.618rem; height: 48px; padding: 0 1.25rem; border-radius: 12px; background: rgba(30, 30, 46, 0.95); border: 2px solid #313244; cursor: pointer; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); display: flex; align-items: center; gap: 0.75rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 9998; backdrop-filter: blur(12px); } .M3Unator-launcher:hover { background: rgba(30, 30, 46, 0.98); border-color: #45475a; transform: translateY(-2px); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3); } .M3Unator-launcher svg { width: 24px; height: 24px; color: #f5c2e7; filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4)); } .M3Unator-launcher span { font-weight: 600; font-size: 0.95rem; background: linear-gradient(90deg, #f5c2e7, #cba6f7, #89b4fa, #a6e3a1, #f5c2e7 ); background-size: 300% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: gradient 3s linear infinite; } @keyframes gradient { 0% { background-position: 0% 50%; filter: hue-rotate(0deg); } 50% { background-position: 100% 50%; filter: hue-rotate(180deg); } 100% { background-position: 0% 50%; filter: hue-rotate(360deg); } } .M3Unator-dropdown { position: relative; width: 100%; } .M3Unator-dropdown-button { width: 100%; padding: 0.618rem; background: #1e1e2e; border: 1px solid #313244; border-radius: 8px; color: #cdd6f4; font-size: 0.875rem; text-align: left; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: space-between; } .M3Unator-dropdown-button:hover { border-color: #45475a; background: rgba(30, 30, 46, 0.8); } .M3Unator-dropdown-button svg { width: 16px; height: 16px; min-width: 16px; min-height: 16px; transition: transform 0.2s ease; } .M3Unator-dropdown.active .M3Unator-dropdown-button { border-color: #cba6f7; border-radius: 8px 8px 0 0; } .M3Unator-dropdown.active .M3Unator-dropdown-button svg { transform: rotate(180deg); } .M3Unator-dropdown-menu { position: absolute; top: 100%; left: 0; right: 0; background: #1e1e2e; border: 1px solid #cba6f7; border-top: none; border-radius: 0 0 8px 8px; overflow: hidden; z-index: 1000; display: none; animation: dropdownSlide 0.2s ease; user-select: none; } .M3Unator-dropdown.active .M3Unator-dropdown-menu { display: block; } .M3Unator-dropdown-item { padding: 0.618rem; color: #cdd6f4; cursor: pointer; transition: all 0.2s ease; user-select: none; } .M3Unator-dropdown-item:hover { background: rgba(203, 166, 247, 0.1); } .M3Unator-dropdown-item.selected { background: rgba(203, 166, 247, 0.1); color: #cba6f7; } @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .M3Unator-log { margin-top: 0.75rem; max-height: calc(100vh - 70vh); font-size: 0.8125rem; line-height: 1.4; } .M3Unator-log:empty { display: none; } .M3Unator-log-entry { padding: 0.25rem 0.5rem; border-bottom: 1px solid #313244; } .M3Unator-log-entry:last-child { border-bottom: none; } .M3Unator-log-entry.success { color: #94e2d5; } .M3Unator-log-entry.error { color: #f38ba8; } .M3Unator-log-entry.warning { color: #fab387; } .M3Unator-log-counter { display: inline-flex; align-items: center; justify-content: center; background: rgba(245, 194, 231, 0.1); color: #f5c2e7; padding: 0.25rem 0.75rem; border-radius: 8px; font-size: 0.875rem; font-weight: 500; margin-left: 0.75rem; min-width: 3rem; text-align: center; } @keyframes gradient { 0% { background-position: 0% 50%; filter: hue-rotate(0deg); } 50% { background-position: 100% 50%; filter: hue-rotate(180deg); } 100% { background-position: 0% 50%; filter: hue-rotate(360deg); } } .M3Unator-title span.text { display: inline-block; position: relative; padding: 0 0.25rem; } .M3Unator-title.scanning span.text { background: linear-gradient(90deg, #f5c2e7, #cba6f7, #89b4fa, #a6e3a1, #f5c2e7 ); background-size: 300% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: gradient 3s linear infinite; font-weight: 700; letter-spacing: 0.5px; } .M3Unator-title.scanning span.text::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 2px; background: inherit; animation: gradient 3s linear infinite; } .M3Unator-title.scanning svg { animation: morphAnimation 2s ease-in-out infinite; filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.5)); } @keyframes morphAnimation { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } } .M3Unator-controls { display: none; gap: 0.75rem; margin: 0.75rem 0; justify-content: center; } .M3Unator-controls.active { display: flex; } .M3Unator-control-btn { display: none; padding: 0.75rem 1.5rem; border-radius: 12px; font-weight: 600; font-size: 0.95rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); align-items: center; gap: 0.75rem; min-width: 160px; justify-content: center; background: rgba(30, 30, 46, 0.6); backdrop-filter: blur(8px); width: 160px; } .M3Unator-control-btn:hover { background: #313244; transform: translateY(-1px); } .M3Unator-control-btn:active { transform: translateY(1px); } .M3Unator-control-btn.pause { border-color: #fab387; color: #fab387; } .M3Unator-control-btn.pause:hover { background: rgba(250, 179, 135, 0.1); } .M3Unator-control-btn.resume { border-color: #94e2d5; color: #94e2d5; } .M3Unator-control-btn.resume:hover { background: rgba(148, 226, 213, 0.1); } .M3Unator-control-btn.cancel { border-color: #f38ba8; color: #f38ba8; } .M3Unator-control-btn.cancel:hover { background: rgba(243, 139, 168, 0.1); } .M3Unator-control-btn svg { width: 14px; height: 14px; } .M3Unator-button { width: 100%; padding: 0 1rem; background: #f5c2e7; color: #11111b; border: none; border-radius: 6px; font-weight: 600; font-size: 0.875rem; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 0.375rem; height: 48px; min-height: 48px; line-height: 1; } .M3Unator-spinner { width: 20px; height: 20px; border: 2px solid rgba(17, 17, 27, 0.3); border-radius: 50%; border-top-color: #11111b; animation: spin 0.6s linear infinite; margin-right: 0; flex-shrink: 0; } .M3Unator-toast-container { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 999999; pointer-events: none; display: flex; flex-direction: column; align-items: center; width: auto; } .M3Unator-toast { display: flex; align-items: center; gap: 12px; padding: 12px 24px; border-radius: 12px; margin-bottom: 12px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 14px; font-weight: 500; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); background: rgba(17, 17, 27, 0.95); border: 2px solid; pointer-events: all; min-width: 300px; max-width: 500px; backdrop-filter: blur(16px); will-change: transform, opacity; animation: none; transform-origin: center bottom; } .M3Unator-toast.show { animation: toastBounceIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .M3Unator-toast.removing { animation: toastBounceOut 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards; } @keyframes toastBounceIn { 0% { opacity: 0; transform: scale(0.3) translateY(2000px); } 60% { opacity: 1; transform: scale(1.1) translateY(-20px); } 75% { transform: scale(0.95) translateY(10px); } 90% { transform: scale(1.02) translateY(-5px); } 100% { transform: scale(1) translateY(0); } } @keyframes toastBounceOut { 0% { transform: scale(1) translateY(0); opacity: 1; } 20% { transform: scale(1.1) translateY(-20px); opacity: 0.8; } 100% { transform: scale(0.3) translateY(2000px); opacity: 0; } } .M3Unator-toast svg { width: 20px; height: 20px; flex-shrink: 0; filter: drop-shadow(0 0 4px currentColor); animation: iconPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; opacity: 0; transform: scale(0.5); } @keyframes iconPop { 0% { opacity: 0; transform: scale(0.5) rotate(-180deg); } 100% { opacity: 1; transform: scale(1) rotate(0deg); } } .M3Unator-toast span { opacity: 0; transform: translateX(-10px); animation: textSlide 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; animation-delay: 0.15s; } @keyframes textSlide { 0% { opacity: 0; transform: translateX(-10px); } 100% { opacity: 1; transform: translateX(0); } } .M3Unator-input-row { display: flex; gap: 0.75rem; margin-bottom: 0.75rem; } .M3Unator-input-row .M3Unator-input-group { margin-bottom: 0; } .M3Unator-input-row .M3Unator-input-group:first-child { flex: 2; } .M3Unator-input-row .M3Unator-input-group:last-child { flex: 1; } .M3Unator-social { display: flex; gap: 8px; margin-right: 8px; } .M3Unator-social a { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; color: #cdd6f4; background: rgba(205, 214, 244, 0.1); transition: all 0.2s ease; } .M3Unator-social a:hover { background: rgba(205, 214, 244, 0.2); transform: rotate(360deg); } .M3Unator-social svg { width: 18px; height: 18px; } .M3Unator-advanced-settings { margin-top: 1rem; padding: 1rem; background: rgba(30, 30, 46, 0.5); border: 1px solid #313244; border-radius: 8px; display: none; } .M3Unator-advanced-settings.active { display: block; animation: fadeIn 0.3s ease; } .M3Unator-advanced-toggle { width: 100%; padding: 0.75rem; background: #1e1e2e; border: 1px solid #313244; border-radius: 8px; color: #cdd6f4; font-size: 0.875rem; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: space-between; transition: all 0.2s ease; } .M3Unator-advanced-toggle:hover { background: #313244; } .M3Unator-advanced-toggle svg { width: 16px; height: 16px; transition: transform 0.2s ease; } .M3Unator-advanced-toggle.active svg { transform: rotate(180deg); } .M3Unator-depth-slider { -webkit-appearance: none; width: 100%; height: 4px; border-radius: 2px; background: #313244; outline: none; margin: 1rem 0; } .M3Unator-depth-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #cba6f7; cursor: pointer; transition: all 0.2s ease; } .M3Unator-depth-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } .M3Unator-depth-value { text-align: center; font-size: 0.875rem; color: #cdd6f4; margin-top: 0.5rem; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .M3Unator-depth-settings { margin-top: 0.75rem; margin-left: 1.75rem; padding: 0.75rem; background: rgba(30, 30, 46, 0.3); border-left: 2px solid #cba6f7; border-radius: 0 8px 8px 0; display: none; animation: slideDown 0.3s ease; } .M3Unator-depth-settings.active { display: block; } .M3Unator-depth-input { position: relative; display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem; } .M3Unator-depth-input input[type="number"] { width: 64px; padding: 0.25rem 0.375rem; border: 1px solid #45475a; border-radius: 4px; background: rgba(30, 30, 46, 0.8); color: #cdd6f4; font-size: 0.875rem; text-align: center; margin: 0 0 0 0.5rem; } .M3Unator-depth-input input[type="number"]:focus { outline: none; border-color: #cba6f7; box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2); } .M3Unator-depth-input input[type="number"]::-webkit-inner-spin-button { opacity: 1; background: #313244; border-left: 1px solid #45475a; border-radius: 0 4px 4px 0; cursor: pointer; } .M3Unator-depth-toggle { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; background: #1e1e2e; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; font-size: 0.875rem; cursor: pointer; transition: all 0.2s ease; } .M3Unator-depth-toggle:hover { background: #313244; border-color: #cba6f7; } .M3Unator-depth-toggle.active { background: rgba(203, 166, 247, 0.1); border-color: #cba6f7; color: #cba6f7; } @keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .M3Unator-stats-bar { margin: 0.75rem 0; padding: 0.5rem; background: rgba(30, 30, 46, 0.5); border: 1px solid #313244; border-radius: 8px; display: none; } .M3Unator-stats-bar.active { display: block; } .M3Unator-stats { display: flex; align-items: center; justify-content: space-around; gap: 0.382rem; padding: 0.25rem; } .M3Unator-stat { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.75rem; color: #cdd6f4; cursor: help; min-width: 40px; justify-content: flex-start; padding: 0 0.25rem; position: relative; } .M3Unator-stat span { min-width: 16px; text-align: right; font-variant-numeric: tabular-nums; font-size: 0.7rem; font-weight: 500; } .M3Unator-stat svg { opacity: 0.8; flex-shrink: 0; width: 14px; height: 14px; } .M3Unator-stat.video { color: #94e2d5; } .M3Unator-stat.audio { color: #89b4fa; } .M3Unator-stat.dir { color: #cba6f7; } .M3Unator-stat.error { color: #f38ba8; } .M3Unator-stat.depth { color: #a6e3a1; transition: color 0.3s ease; } .M3Unator-stat.depth[data-progress="high"] { color: #f38ba8; } .M3Unator-stat.depth[data-progress="medium"] { color: #fab387; } .M3Unator-stat.depth[data-progress="low"] { color: #f9e2af; } .M3Unator-stat:hover::after { content: attr(title); position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%); padding: 0.5rem 0.75rem; background: rgba(30, 30, 46, 0.95); color: #cdd6f4; font-size: 0.875rem; white-space: nowrap; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; border: 1px solid #313244; text-align: center; backdrop-filter: blur(8px); pointer-events: none; } .M3Unator-spinner { width: 20px; height: 20px; border: 2px solid rgba(17, 17, 27, 0.3); border-radius: 50%; border-top-color: #11111b; animation: spin 0.6s linear infinite; margin-right: 0; flex-shrink: 0; } @keyframes spin { to { transform: rotate(360deg); } } .M3Unator-toast { animation: toastSlideUp 0.2s ease forwards; } .M3Unator-toast.removing { animation: toastSlideDown 0.2s ease forwards; } .M3Unator-popup { animation: slideUp 0.2s ease; } .M3Unator-stats-bar { animation: fadeIn 0.2s ease; } .M3Unator-log { transition: max-height 0.3s ease; } .M3Unator-log.collapsed { max-height: 0; overflow: hidden; } .M3Unator-log-toggle { width: 100%; padding: 0.5rem 0.75rem; background: rgba(203, 166, 247, 0.05); border: none; color: #cdd6f4; display: flex; align-items: center; justify-content: space-between; cursor: pointer; transition: all 0.2s ease; font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; border-radius: 6px; } .M3Unator-log-toggle:hover { background: rgba(203, 166, 247, 0.1); } .M3Unator-activity-indicator { width: 14px; height: 14px; border-radius: 50%; background: #45475a; /* Darker gray */ margin-left: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .M3Unator-activity-indicator.active { background: #89dceb; /* Brighter blue */ box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2); animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .M3Unator-activity-indicator.paused { background: #f9e2af; /* More visible yellow */ box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2); } .M3Unator-activity-indicator.error { background: #f38ba8; /* Current red is good */ box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2); } .M3Unator-activity-indicator.completed { background: #94e2d5; /* Brighter green-turquoise */ box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2); } @keyframes pulseActive { 0% { box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(137, 220, 235, 0); } 100% { box-shadow: 0 0 0 0 rgba(137, 220, 235, 0); } } @keyframes pulsePaused { 0% { box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(249, 226, 175, 0); } 100% { box-shadow: 0 0 0 0 rgba(249, 226, 175, 0); } } @keyframes completeScale { 0% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1.2); } 100% { transform: scale(1); opacity: 1; } } .M3Unator-toggle-container span svg .infinity-icon { opacity: 0.5; transition: opacity 0.2s ease; transform: scale(0.6) translateY(4px); transform-origin: center; stroke-width: 1.5; } .M3Unator-toggle-container input[type="checkbox"]:checked + span svg .infinity-icon { opacity: 1; } .M3Unator-depth-controls { background: rgba(30, 30, 46, 0.4); backdrop-filter: blur(8px); border: 1px solid #313244; border-radius: 8px; padding: 0.618rem; margin-top: 1rem; display: none; } .M3Unator-depth-controls.active { display: block; } .M3Unator-radio-group { display: flex; gap: 0.75rem; justify-content: center; background: rgba(30, 30, 46, 0.6); padding: 0.5rem; border-radius: 6px; } .M3Unator-radio { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; padding: 0.5rem; border-radius: 4px; transition: all 0.2s ease; background: transparent; border: 1px solid transparent; } .M3Unator-radio:hover { background: rgba(203, 166, 247, 0.1); } .M3Unator-radio input[type="radio"] { display: none; } .M3Unator-radio .radio-mark { width: 16px; height: 16px; border: 1.5px solid #45475a; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; flex-shrink: 0; background: rgba(30, 30, 46, 0.6); position: relative; } .M3Unator-radio input[type="radio"]:checked + .radio-mark { border-color: #cba6f7; background: rgba(203, 166, 247, 0.1); } .M3Unator-radio input[type="radio"]:checked + .radio-mark::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #cba6f7; position: absolute; } .M3Unator-radio .radio-label { color: #cdd6f4; font-size: 0.875rem; user-select: none; display: flex; align-items: center; gap: 0.5rem; } .M3Unator-depth-input { width: 64px; padding: 0.25rem 0.375rem; border: 1px solid #45475a; border-radius: 4px; background: rgba(30, 30, 46, 0.8); color: #cdd6f4; font-size: 0.875rem; text-align: center; transition: all 0.2s ease; -moz-appearance: textfield; margin-top: -1px; display: inline-flex; align-items: center; height: 28px; } .M3Unator-depth-input::-webkit-outer-spin-button, .M3Unator-depth-input::-webkit-inner-spin-button { -webkit-appearance: inner-spin-button; opacity: 1; background: #313244; border-left: 1px solid #45475a; border-radius: 0 4px 4px 0; cursor: pointer; height: 100%; position: absolute; right: 0; top: 0; } .M3Unator-depth-input:focus { outline: none; border-color: #cba6f7; box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2); } .M3Unator-depth-input:disabled { opacity: 0.5; cursor: not-allowed; background: rgba(30, 30, 46, 0.4); } .M3Unator-radio .radio-label { display: flex; align-items: center; gap: 0.5rem; color: #cdd6f4; font-size: 0.875rem; user-select: none; } .M3Unator-url-container { display: flex; align-items: center; background: rgba(30, 30, 46, 0.6); border: 1px solid #313244; border-radius: 6px; padding: 0.618rem; margin-bottom: 1rem; transition: all 0.2s ease; } .M3Unator-url-container:hover { border-color: #45475a; } .M3Unator-url-icon { color: #6c7086; margin-right: 0.618rem; flex-shrink: 0; } .M3Unator-url-input { flex: 1; background: transparent; border: none; color: #cdd6f4; font-size: 0.875rem; padding: 0; margin: 0; width: 100%; } .M3Unator-url-input:focus { outline: none; } .M3Unator-url-copy { background: transparent; border: none; color: #6c7086; padding: 0.382rem; margin-left: 0.618rem; cursor: pointer; border-radius: 4px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .M3Unator-url-copy:hover { color: #cdd6f4; background: rgba(205, 214, 244, 0.1); } .M3Unator-url-copy.copied { color: #a6e3a1; animation: copyPulse 0.3s ease; } @keyframes copyPulse { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } .M3Unator-toggle-group { display: flex; gap: 1.25rem; margin: 1.5rem 0; justify-content: center; background: rgba(30, 30, 46, 0.4); padding: 1.25rem; border-radius: 16px; backdrop-filter: blur(8px); } .M3Unator-toggle-container { position: relative; } .M3Unator-toggle-container span { width: 64px; height: 64px; background: #1e1e2e; border: 2px solid #45475a; border-radius: 16px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; justify-content: center; } .M3Unator-toggle-container input[type="checkbox"]:checked + span { background: rgba(203, 166, 247, 0.1); border-color: #cba6f7; box-shadow: 0 0 20px rgba(203, 166, 247, 0.2); transform: translateY(-2px); } .M3Unator-toggle-container span:hover { background: #313244; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } .M3Unator-toggle-container svg { width: 32px; height: 32px; opacity: 0.7; transition: all 0.3s ease; } .M3Unator-toggle-container input[type="checkbox"]:checked + span svg { opacity: 1; color: #cba6f7; filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4)); } .M3Unator-progress { background: rgba(30, 30, 46, 0.6); border-radius: 12px; padding: 1rem; margin: 1rem 0; backdrop-filter: blur(8px); border: 1px solid rgba(203, 166, 247, 0.2); } .M3Unator-progress-text { color: #f5c2e7; font-weight: 600; text-align: center; margin-bottom: 0.5rem; font-size: 1.1rem; } .M3Unator-progress-spinner { width: 24px; height: 24px; border: 3px solid rgba(245, 194, 231, 0.1); border-top-color: #f5c2e7; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; } .M3Unator-controls { display: flex; gap: 0.75rem; margin: 0.75rem 0; justify-content: center; } .M3Unator-control-btn { display: none; padding: 0.75rem 1.5rem; border-radius: 12px; font-weight: 600; font-size: 0.95rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); align-items: center; gap: 0.75rem; min-width: 160px; justify-content: center; background: rgba(30, 30, 46, 0.6); backdrop-filter: blur(8px); width: 160px; } .M3Unator-control-btn.pause { background: rgba(250, 179, 135, 0.1); border: 2px solid #fab387; color: #fab387; } .M3Unator-control-btn.resume { background: rgba(148, 226, 213, 0.1); border: 2px solid #94e2d5; color: #94e2d5; } .M3Unator-control-btn.cancel { background: rgba(243, 139, 168, 0.1); border: 2px solid #f38ba8; color: #f38ba8; } .M3Unator-control-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } .M3Unator-control-btn svg { width: 20px; height: 20px; } .M3Unator-layers-icon { width: 20px; height: 20px; margin-right: 0.5rem; } .M3Unator-input:-webkit-autofill, .M3Unator-input:-webkit-autofill:hover, .M3Unator-input:-webkit-autofill:focus, .M3Unator-input:-webkit-autofill:active { -webkit-text-fill-color: #cdd6f4 !important; -webkit-box-shadow: 0 0 0 30px #1e1e2e inset !important; box-shadow: 0 0 0 30px #1e1e2e inset !important; background-color: #1e1e2e !important; color: #cdd6f4 !important; caret-color: #cdd6f4 !important; transition: background-color 5000s ease-in-out 0s !important; text-decoration: none !important; -webkit-text-decoration: none !important; } .M3Unator-input:-moz-autofill, .M3Unator-input:-moz-autofill-preview { background-color: #1e1e2e !important; color: #cdd6f4 !important; text-decoration: none !important; } .M3Unator-input:-ms-input-placeholder { background-color: #1e1e2e !important; color: #cdd6f4 !important; text-decoration: none !important; } .M3Unator-log-container { margin: 0; } .M3Unator-log-toggle { width: 100%; padding: 0.5rem 0.75rem; background: rgba(203, 166, 247, 0.05); border: none; color: #cdd6f4; display: flex; align-items: center; justify-content: space-between; cursor: pointer; transition: all 0.2s ease; font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; border-radius: 6px; } .M3Unator-log-toggle:hover { background: rgba(203, 166, 247, 0.1); } .M3Unator-activity-indicator { width: 14px; height: 14px; border-radius: 50%; background: #45475a; /* Darker gray */ margin-left: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .M3Unator-activity-indicator.active { background: #89dceb; /* Brighter blue */ box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2); animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .M3Unator-activity-indicator.paused { background: #f9e2af; /* More visible yellow */ box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2); } .M3Unator-activity-indicator.error { background: #f38ba8; /* Current red is good */ box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2); } .M3Unator-activity-indicator.completed { background: #94e2d5; /* Brighter green-turquoise */ box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2); } @keyframes pulseActive { 0% { box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(137, 220, 235, 0); } 100% { box-shadow: 0 0 0 0 rgba(137, 220, 235, 0); } } @keyframes pulsePaused { 0% { box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(249, 226, 175, 0); } 100% { box-shadow: 0 0 0 0 rgba(249, 226, 175, 0); } } @keyframes completeScale { 0% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1.2); } 100% { transform: scale(1); opacity: 1; } } .M3Unator-log-toggle:hover .M3Unator-activity-indicator { background: #6c7086; animation: none; } .M3Unator-log-toggle.active .M3Unator-activity-indicator { background: #6c7086; animation: none; } .M3Unator-log-toggle .toggle-text { display: flex; align-items: center; gap: 0.5rem; } .M3Unator-log { height: 0; max-height: 0; overflow: hidden; transition: all 0.3s ease; background: #11111b; padding: 0; border-top: none; margin: 0; } .M3Unator-log.expanded { height: auto; max-height: 300px; padding: 0.75rem; border-top: 1px solid #313244; overflow-y: auto; } .M3Unator-log-entry { padding: 0.25rem 0.5rem; border-bottom: 1px solid rgba(49, 50, 68, 0.5); font-size: 0.875rem; } .M3Unator-log-entry:last-child { border-bottom: none; } .M3Unator-log-time { color: #6c7086; margin-right: 0.5rem; } .M3Unator-log-entry.success { color: #94e2d5; } .M3Unator-log-entry.error { color: #f38ba8; } .M3Unator-log-entry.warning { color: #fab387; } .M3Unator-log-entry.info { color: #89b4fa; } .M3Unator-log-entry.final { color: #a6e3a1; font-weight: 500; } .M3Unator-log { margin-top: 0.75rem; max-height: calc(100vh - 70vh); font-size: 0.8125rem; line-height: 1.4; } .M3Unator-log-entry { padding: 0.25rem 0.5rem; border-radius: 4px; } .M3Unator-log-toggle { padding: 10px 12px; height: 42px; display: flex; align-items: center; justify-content: space-between; } .M3Unator-log-counter { padding: 0.125rem 0.375rem; font-size: 0.75rem; border-radius: 4px; } .M3Unator-log-time { font-size: 0.75rem; opacity: 0.7; margin-right: 0.5rem; } `); GM_addStyle(` .M3Unator-popup { position: fixed; background: #11111b; color: #cdd6f4; width: 100%; max-width: 480px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); overflow: hidden; animation: slideUp 0.3s ease; z-index: 9999; } .M3Unator-header { padding: 1rem 1.25rem; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: space-between; cursor: move; user-select: none; border-bottom: 1px solid #313244; } .M3Unator-container { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(8px); display: none; place-items: center; z-index: 9999; } `); GM_addStyle(` /* Info Modal Styles */ .info-modal { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(8px); z-index: 10000; } .info-modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e2e; border: 1px solid #45475a; border-radius: 12px; width: 90%; max-width: 600px; color: #cdd6f4; } .info-modal-header { padding: 1rem 1.5rem; border-bottom: 1px solid #45475a; display: flex; align-items: center; justify-content: space-between; } .info-modal-header h3 { margin: 0; color: #f5c2e7; font-size: 1.25rem; } .info-modal-body { padding: 1.5rem; line-height: 1.6; } .info-modal-body p { margin: 0 0 1rem; } .info-modal-body h4 { margin: 1.5rem 0 0.75rem; color: #f5c2e7; } .info-modal-body ul { margin: 0.75rem 0; padding-left: 1.5rem; } .info-modal-body li { margin: 0.5rem 0; } .info-modal-body a { color: #89b4fa; text-decoration: none; } .info-modal-body a:hover { text-decoration: underline; } .info-close { cursor: pointer; color: #6c7086; transition: color 0.2s ease; } .info-close:hover { color: #f5c2e7; } `); GM_addStyle(` .m3unator-input-group { position: relative; width: 100%; } .m3unator-input { width: 100%; padding-right: 80px !important; transition: all 0.2s ease; } .m3unator-dropdown { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); display: none; z-index: 1; width: 70px; } .m3unator-dropdown.active { display: block; } .m3unator-dropdown-button { width: 100%; padding: 4px 8px; border-radius: 6px; background: rgba(30, 30, 46, 0.6); border: 1px solid rgba(69, 71, 90, 0.6); color: #f5c2e7; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 4px; transition: all 0.2s ease; } .m3unator-dropdown-button:hover { background: rgba(30, 30, 46, 0.8); border-color: rgba(69, 71, 90, 0.8); } .m3unator-dropdown-menu { position: absolute; top: 100%; right: 0; width: 100%; margin-top: 4px; background: rgba(30, 30, 46, 0.95); border: 1px solid rgba(69, 71, 90, 0.6); border-radius: 6px; padding: 4px; display: none; } .m3unator-dropdown.active .m3unator-dropdown-menu { display: block; } .m3unator-dropdown-item { padding: 0.618rem; color: #cdd6f4; cursor: pointer; transition: all 0.2s ease; user-select: none; } .m3unator-dropdown-item:hover { background: rgba(203, 166, 247, 0.1); } .m3unator-dropdown-item.selected { background: rgba(203, 166, 247, 0.1); color: #cba6f7; } `); GM_addStyle(` .M3Unator-container { max-width: 400px; width: 100%; background: none; backdrop-filter: none; } .M3Unator-popup { background: #1e1e2e; border-radius: 12px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); border: 1px solid rgba(69, 71, 90, 0.6); } .M3Unator-content { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem; max-width: 100%; overflow: hidden; background: none; } .M3Unator-header { padding: 0.75rem; display: flex; align-items: center; justify-content: space-between; background: none; border-bottom: 1px solid rgba(69, 71, 90, 0.6); } .M3Unator-input { width: 100%; min-width: 0; padding: 8px 80px 8px 12px; box-sizing: border-box; transition: all 0.2s ease; background: #1e1e2e; border: 1px solid rgba(69, 71, 90, 0.6); border-radius: 6px; color: #f5c2e7; font-size: 14px; } .M3Unator-dropdown-button { width: 100%; padding: 4px 8px; border-radius: 6px; background: #1e1e2e; border: 1px solid rgba(69, 71, 90, 0.6); color: #f5c2e7; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 4px; transition: all 0.2s ease; box-sizing: border-box; font-size: 14px; font-family: monospace; } .M3Unator-dropdown-button span { min-width: 40px; text-align: left; } .M3Unator-dropdown-button svg { width: 16px; height: 16px; min-width: 16px; min-height: 16px; margin-left: auto; } .M3Unator-dropdown-menu { position: absolute; top: 100%; right: 0; width: 100%; margin-top: 4px; background: #1e1e2e; border: 1px solid rgba(69, 71, 90, 0.6); border-radius: 6px; padding: 4px; display: none; box-sizing: border-box; z-index: 9999; } `); GM_addStyle(` .M3Unator-content { padding: 0.75rem; display: flex; flex-direction: column; gap: 12px; } .M3Unator-toggle-group { margin: 0; display: flex; gap: 0.75rem; justify-content: center; background: rgba(30, 30, 46, 0.4); padding: 0.75rem; border-radius: 12px; } .M3Unator-button { margin: 0; } .M3Unator-log-container { margin: 0; } .M3Unator-stats-bar { margin: 0; } `); GM_addStyle(` /* Dropdown Styles */ .M3Unator-dropdown { position: relative; display: none; } .M3Unator-dropdown-button { width: 100%; padding: 4px 8px; border-radius: 6px; background: #1e1e2e; border: 1px solid rgba(69, 71, 90, 0.6); color: #f5c2e7; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 4px; transition: all 0.2s ease; box-sizing: border-box; font-size: 14px; font-family: monospace; } .M3Unator-dropdown-button span { min-width: 40px; text-align: left; } .M3Unator-dropdown-button svg { width: 16px; height: 16px; min-width: 16px; min-height: 16px; margin-left: auto; transition: transform 0.2s ease; } .M3Unator-dropdown.active .M3Unator-dropdown-button svg { transform: rotate(180deg); } .M3Unator-dropdown-menu { position: absolute; top: 100%; left: 0; right: 0; margin-top: 4px; background: #1e1e2e; border: 1px solid rgba(69, 71, 90, 0.6); border-radius: 6px; overflow: hidden; z-index: 1000; display: none; } .M3Unator-dropdown.active .M3Unator-dropdown-menu { display: block; } .M3Unator-dropdown-item { padding: 6px 12px; color: #f5c2e7; cursor: pointer; transition: all 0.2s ease; font-family: monospace; } .M3Unator-dropdown-item:hover { background: rgba(69, 71, 90, 0.3); } .M3Unator-dropdown-item:not(:last-child) { border-bottom: 1px solid rgba(69, 71, 90, 0.3); } /* Input Styles */ .M3Unator-input { width: 100%; height: 42px; padding: 0 12px; border: 1px solid #45475a; border-radius: 8px; background: #1e1e2e; color: #f5c2e7; font-size: 14px; transition: all 0.2s ease; box-sizing: border-box; } .M3Unator-input:focus { outline: none; border-color: #f5c2e7; box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1); } /* Button Styles */ .M3Unator-button { height: 42px; padding: 0 16px; border: none; border-radius: 8px; background: #f5c2e7; color: #1e1e2e; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } /* Toggle Container Styles */ .M3Unator-toggle-container { position: relative; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; } /* Control Button Styles */ .M3Unator-control-btn { padding: 0.75rem 1.5rem; border-radius: 12px; font-weight: 600; font-size: 0.95rem; min-width: 160px; background: rgba(30, 30, 46, 0.6); backdrop-filter: blur(8px); } .M3Unator-control-btn.pause { border-color: #fab387; color: #fab387; } .M3Unator-control-btn.resume { border-color: #94e2d5; color: #94e2d5; } .M3Unator-control-btn.cancel { border-color: #f38ba8; color: #f38ba8; } /* Stats Styles */ .M3Unator-stat { display: inline-flex; align-items: center; gap: 0.382rem; font-size: 0.875rem; cursor: help; min-width: 52px; padding: 0 0.382rem; } `); GM_addStyle(` .M3Unator-toast.success { color: #a6e3a1; border-color: #a6e3a1; background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(166, 227, 161, 0.1); } .M3Unator-toast.error { color: #f38ba8; border-color: #f38ba8; background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(243, 139, 168, 0.1); } .M3Unator-toast.warning { color: #fab387; border-color: #fab387; background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(250, 179, 135, 0.1); } `); class LogCache { constructor(maxSize = 100) { this.maxSize = maxSize; this.logs = []; this.stats = { totalLogs: 0, skippedLogs: 0 }; } add(message, type = '') { const timestamp = new Date().toLocaleTimeString(); this.logs.push({ message, type, timestamp }); this.stats.totalLogs++; if (this.logs.length > this.maxSize) { this.logs.shift(); this.stats.skippedLogs++; } } getSummary() { return { logs: [...this.logs], stats: { ...this.stats } }; } clear() { this.logs = []; this.stats.totalLogs = 0; this.stats.skippedLogs = 0; } } class PlaylistGenerator { constructor() { this.initialStats = { directories: { total: 0, depth: 0 }, files: { video: { total: 0, current: 0 }, audio: { total: 0, current: 0 } }, errors: { total: 0, skipped: 0 }, totalFiles: 0 }; this.videoFormats = [ '.mp4', '.mkv', '.avi', '.webm', '.mov', '.flv', '.wmv', '.m4v', '.mpg', '.mpeg', '.3gp', '.vob', '.ts', '.mts', '.m2ts', '.divx', '.xvid', '.asf', '.ogv', '.rm', '.rmvb', '.wtv', '.qt', '.hevc', '.f4v', '.swf', '.vro', '.ogx', '.drc', '.gifv', '.mxf', '.roq', '.nsv' ]; this.audioFormats = [ '.mp3', '.m4a', '.wav', '.flac', '.aac', '.ogg', '.wma', '.opus', '.aiff', '.ape', '.mka', '.ac3', '.dts', '.m4b', '.m4p', '.m4r', '.mid', '.midi', '.mp2', '.mpa', '.mpc', '.ra', '.tta', '.voc', '.vox', '.amr', '.awb', '.dsf', '.dff', '.alac', '.wv', '.oga', '.sln', '.aif', '.pcm' ]; // Create Map for file extensions this.extensionMap = new Map(); // Add video extensions to Map this.videoFormats.forEach(ext => { this.extensionMap.set(ext.slice(1), 'video'); // .mp4 -> mp4 }); // Add audio extensions to Map this.audioFormats.forEach(ext => { this.extensionMap.set(ext.slice(1), 'audio'); // .mp3 -> mp3 }); this.domElements = {}; this.state = { isGenerating: false, isPaused: false, selectedFormat: 'm3u', includeVideo: false, includeAudio: false, maxEntries: 1000000, timeoutMs: 5000, retryCount: 2, maxDepth: 0, maxSeenUrls: 5000, stats: { ...this.initialStats } }; this.sortOptions = { numeric: true, sensitivity: 'base' }; this.entries = []; this.seenUrls = new Set(); this.toastQueue = []; this.isProcessingToast = false; this.icons = { video: ` `, audio: ` `, folder: ` `, info: ` `, file: ` `, download: ` `, close: ` `, pause: ` `, resume: ` `, cancel: ` `, success: ` `, error: ` `, warning: ` `, github: ` `, twitter: ` `, chevronDown: ` `, layers: ` `, logToggle: ` ` }; this.templates = { toggleButton: (id, title, icon, checked = false) => `
`, controlButton: (type, icon, text) => ` `, statsItem: (icon, id, title, className = '') => ` ${icon} 0 ` }; this.baseStyles = ` .M3Unator-btn-base { border: none; border-radius: 8px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .M3Unator-toggle-base { position: relative; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; } .M3Unator-control-base { padding: 0.75rem 1.5rem; border-radius: 12px; font-weight: 600; font-size: 0.95rem; min-width: 160px; background: rgba(30, 30, 46, 0.6); backdrop-filter: blur(8px); } .M3Unator-stat-base { display: inline-flex; align-items: center; gap: 0.382rem; font-size: 0.875rem; cursor: help; min-width: 52px; padding: 0 0.382rem; } .M3Unator-icon-base { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; transition: all 0.2s ease; } `; GM_addStyle(this.baseStyles); this.updateActivityIndicator = (status) => { const indicator = this.domElements.activityIndicator; if (!indicator) return; // Remove all classes first indicator.classList.remove('active', 'paused', 'cancelled', 'completed'); // Status check if (this.state.isGenerating) { if (this.state.isPaused) { indicator.classList.add('paused'); } else { indicator.classList.add('active'); } } else if (status === 'cancelled') { indicator.classList.add('cancelled'); } else if (status === 'completed') { indicator.classList.add('completed'); } }; this.logCache = new LogCache(100); } createComponent(type, props) { switch (type) { case 'toggle': return this.templates.toggleButton( props.id, props.title, props.icon, props.checked ); case 'control': return this.templates.controlButton( props.type, props.icon, props.text ); case 'stats': return ` ${props.icon} 0 `; default: return ''; } } async init() { const container = document.createElement('div'); container.className = 'M3Unator-container'; const toggleButtons = [ { id: 'includeVideo', title: 'Video (.mp4, .mkv)', icon: this.icons.video, checked: true }, { id: 'includeAudio', title: 'Audio (.mp3, .m4a)', icon: this.icons.audio, checked: true }, { id: 'recursiveSearch', title: 'Scan Subdirectories', icon: this.icons.folder, checked: true } ].map(props => this.createComponent('toggle', props)).join(''); const controlButtons = [ { type: 'pause', icon: this.icons.pause, text: 'Pause' }, { type: 'resume', icon: this.icons.resume, text: 'Resume' }, { type: 'cancel', icon: this.icons.cancel, text: 'Cancel' } ].map(props => this.createComponent('control', props)).join(''); const statsItems = [ { icon: this.icons.file, id: 'totalFiles', title: 'Total Files', class: '' }, { icon: this.icons.video, id: 'videoFiles', title: 'Video (.mp4, .mkv)', class: 'video' }, { icon: this.icons.audio, id: 'audioFiles', title: 'Audio (.mp3, .m4a)', class: 'audio' }, { icon: this.icons.folder, id: 'directories', title: 'Subdirectories', class: 'dir' }, { icon: this.icons.layers, id: 'depthLevel', title: 'Depth Level', class: 'depth' }, { icon: this.icons.error, id: 'errors', title: 'Error', class: 'error' } ].map(props => this.createComponent('stats', props)).join(''); container.innerHTML = `

${this.icons.video} M3Unator

About M3Unator

${this.icons.close}

M3Unator v1.0.2 - The Ultimate Web Directory Playlist Creator

Create M3U/M3U8 playlists effortlessly from any web directory. Experience ultrafast scanning and intelligent media detection.

Key Features:

  • ⚡ Ultrafast directory scanning with parallel processing
  • 🎥 Comprehensive media support (MP4, MKV, MP3, FLAC, etc.)
  • 🔍 Smart recursive directory scanning
  • 🛡️ Enhanced error handling and stability
  • 🌙 Modern dark theme interface

For updates and more information, visit the GitHub repository.

.m3u
.m3u8
${toggleButtons}
${controlButtons}
${statsItems}
`; document.body.appendChild(container); const launcher = document.createElement('button'); launcher.className = 'M3Unator-launcher'; launcher.innerHTML = ` ${this.icons.video} M3Unator `; document.body.appendChild(launcher); const popup = container.querySelector('.M3Unator-popup'); const header = container.querySelector('.M3Unator-header'); this.makeDraggable(popup, header); const statsBar = container.querySelector('.M3Unator-stats-bar'); if (statsBar) { statsBar.style.display = 'block'; } this.domElements = { container, popup: container.querySelector('.M3Unator-popup'), header: container.querySelector('.M3Unator-header'), closeBtn: container.querySelector('.M3Unator-close'), generateBtn: container.querySelector('#generateBtn'), playlistInput: container.querySelector('#playlistName'), includeVideo: container.querySelector('#includeVideo'), includeAudio: container.querySelector('#includeAudio'), recursiveSearch: container.querySelector('#recursiveSearch'), controls: container.querySelector('.M3Unator-controls'), scanLog: container.querySelector('#scanLog'), statsBar: container.querySelector('.M3Unator-stats-bar'), dropdown: container.querySelector('.M3Unator-dropdown'), launcher, stats: { totalFiles: container.querySelector('#totalFiles'), videoFiles: container.querySelector('#videoFiles'), audioFiles: container.querySelector('#audioFiles'), directories: container.querySelector('#directories'), depthLevel: container.querySelector('#depthLevel'), errors: container.querySelector('#errors') }, depthControls: container.querySelector('.M3Unator-depth-controls'), currentDepth: container.querySelector('#currentDepth'), customDepth: container.querySelector('#customDepth'), maxDepth: container.querySelector('#maxDepth'), logToggle: container.querySelector('.M3Unator-log-toggle'), logCounter: container.querySelector('.M3Unator-log-counter'), activityIndicator: container.querySelector('.M3Unator-activity-indicator'), }; launcher.onclick = () => { this.domElements.container.setAttribute('data-visible', 'true'); const overlay = document.createElement('div'); overlay.className = 'M3Unator-overlay'; document.body.appendChild(overlay); const popup = this.domElements.popup; const rect = popup.getBoundingClientRect(); const centerX = (window.innerWidth - rect.width) / 2; const centerY = (window.innerHeight - rect.height) / 2; popup.style.left = `${centerX}px`; popup.style.top = `${centerY}px`; }; document.querySelector('.M3Unator-close').onclick = () => { if (this.state.isGenerating) { this.state.isGenerating = false; this.state.isPaused = false; this.reset({ isCancelled: true, enableToggles: true }); this.showToast('Scan cancelled', 'warning'); } this.domElements.container.removeAttribute('data-visible'); const overlay = document.querySelector('.M3Unator-overlay'); if (overlay) overlay.remove(); }; this.setupPopupHandlers(); this.updateCounter(0); this.domElements.logToggle.addEventListener('click', () => { const log = this.domElements.scanLog; const toggle = this.domElements.logToggle; if (log.classList.contains('expanded')) { log.classList.remove('expanded'); toggle.classList.remove('active'); } else { log.classList.add('expanded'); toggle.classList.add('active'); log.scrollTop = log.scrollHeight; } }); this.domElements.scanLog.classList.remove('expanded'); this.domElements.logToggle.classList.remove('active'); this.logCount = 0; } updateStyles() { GM_addStyle(` .M3Unator-toggle-container { @extend .M3Unator-toggle-base; } .M3Unator-control-btn { @extend .M3Unator-control-base; } .M3Unator-stat { @extend .M3Unator-stat-base; } .M3Unator-toggle-container span { @extend .M3Unator-icon-base; background: #1e1e2e; border: 2px solid #45475a; border-radius: 16px; } .M3Unator-control-btn.pause { border-color: #fab387; color: #fab387; } .M3Unator-control-btn.resume { border-color: #94e2d5; color: #94e2d5; } .M3Unator-control-btn.cancel { border-color: #f38ba8; color: #f38ba8; } `); } makeDraggable(element, handle) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; const centerWindow = () => { const rect = element.getBoundingClientRect(); const centerX = (window.innerWidth - rect.width) / 2; const centerY = (window.innerHeight - rect.height) / 2; element.style.left = `${centerX}px`; element.style.top = `${centerY}px`; xOffset = centerX; yOffset = centerY; element.style.transform = 'none'; }; centerWindow(); const getPosition = (e) => { return { x: e.type.includes('touch') ? e.touches[0].clientX : e.clientX, y: e.type.includes('touch') ? e.touches[0].clientY : e.clientY }; }; const dragStart = (e) => { if (e.target === handle || handle.contains(e.target)) { e.preventDefault(); const pos = getPosition(e); isDragging = true; const rect = element.getBoundingClientRect(); xOffset = rect.left; yOffset = rect.top; initialX = pos.x - xOffset; initialY = pos.y - yOffset; handle.style.cursor = 'grabbing'; } }; const drag = (e) => { if (isDragging) { e.preventDefault(); const pos = getPosition(e); currentX = pos.x - initialX; currentY = pos.y - initialY; const rect = element.getBoundingClientRect(); const maxX = window.innerWidth - rect.width; const maxY = window.innerHeight - rect.height; currentX = Math.min(Math.max(0, currentX), maxX); currentY = Math.min(Math.max(0, currentY), maxY); element.style.left = `${currentX}px`; element.style.top = `${currentY}px`; xOffset = currentX; yOffset = currentY; } }; const dragEnd = () => { if (isDragging) { isDragging = false; handle.style.cursor = 'grab'; } }; handle.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); handle.addEventListener('touchstart', dragStart, { passive: false }); document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('touchend', dragEnd); window.addEventListener('resize', () => { if (!isDragging) { centerWindow(); } }); handle.style.cursor = 'grab'; handle.style.userSelect = 'none'; handle.style.touchAction = 'none'; element.style.position = 'fixed'; element.style.margin = '0'; element.style.touchAction = 'none'; element.style.transition = 'none'; } showToast(message, type = 'success', duration = 3000) { let toastContainer = document.querySelector('.M3Unator-toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'M3Unator-toast-container'; document.body.appendChild(toastContainer); } // Remove previous toasts const existingToasts = toastContainer.querySelectorAll('.M3Unator-toast'); existingToasts.forEach(toast => { toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); }); const toast = document.createElement('div'); toast.className = `M3Unator-toast ${type}`; const icon = this.icons[type] || this.icons.info; toast.innerHTML = `${icon}${message}`; toastContainer.appendChild(toast); // Force a reflow to ensure the animation plays void toast.offsetWidth; // Add show class to trigger animation requestAnimationFrame(() => { toast.classList.add('show'); }); setTimeout(() => { toast.classList.add('removing'); toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode === toastContainer) { toast.remove(); } if (toastContainer.children.length === 0) { toastContainer.remove(); } }, 300); }, duration); } setupPopupHandlers() { const generateBtn = this.domElements.generateBtn; const playlistInput = this.domElements.playlistInput; const includeVideo = this.domElements.includeVideo; const includeAudio = this.domElements.includeAudio; const recursiveSearch = this.domElements.recursiveSearch; const controls = this.domElements.controls; const dropdown = this.domElements.dropdown; const dropdownButton = dropdown.querySelector('.M3Unator-dropdown-button'); const dropdownItems = dropdown.querySelectorAll('.M3Unator-dropdown-item'); const controlButtons = controls.querySelectorAll('.M3Unator-control-btn'); const pauseBtn = controlButtons[0]; const resumeBtn = controlButtons[1]; const cancelBtn = controlButtons[2]; dropdownButton.addEventListener('click', () => { dropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target)) { dropdown.classList.remove('active'); } }); dropdownItems.forEach(item => { item.addEventListener('click', () => { dropdownItems.forEach(i => i.classList.remove('selected')); item.classList.add('selected'); dropdownButton.querySelector('span').textContent = item.textContent; this.state.selectedFormat = item.dataset.value; dropdown.classList.remove('active'); }); }); recursiveSearch.checked = true; this.state.recursiveSearch = true; includeVideo.checked = true; includeAudio.checked = true; this.state.includeVideo = true; this.state.includeAudio = true; includeVideo.addEventListener('change', (e) => { this.state.includeVideo = e.target.checked; this.addLogEntry( e.target.checked ? 'Video files will be included' : 'Video files will not be included', 'info' ); }); includeAudio.addEventListener('change', (e) => { this.state.includeAudio = e.target.checked; this.addLogEntry( e.target.checked ? 'Audio files will be included' : 'Audio files will not be included', 'info' ); }); const currentDepth = this.domElements.currentDepth; const customDepth = this.domElements.customDepth; const maxDepth = this.domElements.maxDepth; const depthControls = this.domElements.depthControls; depthControls.style.display = 'none'; depthControls.classList.remove('active'); this.state.maxDepth = -1; currentDepth.checked = true; customDepth.checked = false; maxDepth.disabled = true; maxDepth.value = '1'; recursiveSearch.addEventListener('change', (e) => { if (!e.target.checked) { depthControls.style.display = 'block'; depthControls.classList.add('active'); currentDepth.checked = true; customDepth.checked = false; maxDepth.disabled = true; this.state.maxDepth = 0; this.addLogEntry('Directory scanning disabled, only current directory will be scanned', 'info'); } else { depthControls.style.display = 'none'; depthControls.classList.remove('active'); this.state.maxDepth = -1; this.state.recursiveSearch = true; this.addLogEntry('Directory scanning active, all directories will be scanned', 'info'); } }); this.domElements.currentDepth.addEventListener('change', (e) => { if (e.target.checked && !recursiveSearch.checked) { this.state.maxDepth = 0; this.domElements.maxDepth.disabled = true; this.addLogEntry('Only current directory will be scanned', 'info'); } }); this.domElements.customDepth.addEventListener('change', (e) => { if (e.target.checked && !recursiveSearch.checked) { const depthValue = parseInt(this.domElements.maxDepth.value) || 1; this.state.maxDepth = depthValue; this.domElements.maxDepth.disabled = false; this.addLogEntry( `Directory scanning depth: ${depthValue} ` + `(current directory + ${depthValue} sublevels)`, 'info' ); } }); this.domElements.maxDepth.addEventListener('input', (e) => { if (this.domElements.customDepth.checked && !recursiveSearch.checked) { const value = Math.min(99, Math.max(1, parseInt(e.target.value) || 1)); e.target.value = value; this.state.maxDepth = value; this.addLogEntry( `Directory scanning depth updated: ${value} ` + `(current directory + ${value} sublevels)`, 'info' ); } }); pauseBtn.addEventListener('click', () => { this.state.isPaused = true; this.updateActivityIndicator('paused'); pauseBtn.style.display = 'none'; resumeBtn.style.display = 'flex'; generateBtn.innerHTML = `
Scan paused `; this.showToast('Scan paused', 'warning'); this.addLogEntry('Scan paused...', 'warning'); }); resumeBtn.addEventListener('click', () => { this.state.isPaused = false; this.updateActivityIndicator('active'); resumeBtn.style.display = 'none'; pauseBtn.style.display = 'flex'; generateBtn.innerHTML = `
Creating... `; this.showToast('Scan resumed', 'success'); this.addLogEntry('Scan in progress...', 'success'); }); cancelBtn.addEventListener('click', () => { this.state.isGenerating = false; this.state.isPaused = false; this.updateActivityIndicator('cancelled'); setTimeout(() => { this.reset({ isCancelled: true, enableToggles: true }); this.showToast('Scan cancelled', 'warning'); }, 100); }); generateBtn.addEventListener('click', async () => { const playlistName = this.sanitizeInput(playlistInput.value.trim()); if (!playlistName) { this.showToast('Please enter a valid playlist name', 'warning'); playlistInput.focus(); return; } if (!this.state.includeVideo && !this.state.includeAudio) { this.showToast('Please select at least one media type', 'warning'); return; } try { this.entries = []; this.seenUrls.clear(); this.logCount = 0; if (this.domElements.scanLog) { this.domElements.scanLog.innerHTML = ''; } this.state.stats = JSON.parse(JSON.stringify(this.initialStats)); this.state.isGenerating = true; this.state.isPaused = false; this.updateActivityIndicator('active'); this.showToast('Scan started', 'success'); generateBtn.disabled = true; generateBtn.innerHTML = `
Creating... `; this.domElements.includeVideo.disabled = true; this.domElements.includeAudio.disabled = true; this.domElements.recursiveSearch.disabled = true; this.domElements.currentDepth.disabled = true; this.domElements.customDepth.disabled = true; this.domElements.maxDepth.disabled = true; controls.style.display = 'flex'; controls.classList.add('active'); if (pauseBtn) { pauseBtn.style.display = 'flex'; resumeBtn.style.display = 'none'; cancelBtn.style.display = 'flex'; } this.domElements.statsBar.style.display = 'block'; this.domElements.statsBar.classList.add('active'); const entries = await this.scanDirectory(window.location.href, '', 0); if (!this.state.isGenerating) { return; } if (entries.length === 0) { this.state.isGenerating = false; this.updateActivityIndicator('cancelled'); this.showToast('No media files found', 'error'); this.reset({ isCancelled: true }); return; } this.addLogEntry(`Total ${entries.length} files found.`, 'success'); this.updateCounter(entries.length); const content = this.createPlaylist(entries); const fileName = `${playlistName}.${this.state.selectedFormat}`; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showToast(`Playlist "${fileName}" created successfully`, 'success'); this.updateActivityIndicator('completed'); this.reset({ keepLogs: true, keepUI: true, enableToggles: true }); } catch (error) { this.state.isGenerating = false; this.state.isPaused = false; this.updateActivityIndicator('cancelled'); console.error('Error creating playlist:', error); this.addLogEntry(`Error: ${error.message}`, 'error'); this.showToast('Error creating playlist', 'error'); this.reset({ isCancelled: true }); } }); } reset(options = {}) { const { isCancelled = false, uiOnly = false, keepLogs = false, keepUI = false, enableToggles = false, wasGenerating = this.state.isGenerating } = options; // Save current state before updating const wasPaused = this.state.isPaused; this.state.isGenerating = false; this.state.isPaused = false; // Update activity indicator if (isCancelled || wasPaused) { this.updateActivityIndicator('cancelled'); } else if (wasGenerating) { this.updateActivityIndicator('completed'); } else { this.updateActivityIndicator(null); } if (!uiOnly) { this.entries = []; this.seenUrls.clear(); if (!keepLogs) { this.logCount = 0; if (this.domElements.scanLog) { this.domElements.scanLog.innerHTML = ''; } if (this.domElements.logCounter) { this.domElements.logCounter.textContent = '0'; } } if (wasGenerating && !isCancelled) { const stats = this.domElements.stats; const summary = [ `Scan completed:`, `• Video files: ${stats.videoFiles.textContent}`, `• Audio files: ${stats.audioFiles.textContent}`, `• Scanned directories: ${stats.directories.textContent}`, `• Maximum depth: ${stats.depthLevel.textContent}`, stats.errors.textContent > 0 ? `• Errors: ${stats.errors.textContent} (${this.state.stats.errors.skipped} skipped)` : null ].filter(Boolean).join('\n'); this.addLogEntry(summary, 'final'); } } const elements = this.domElements; if (elements.generateBtn) { elements.generateBtn.disabled = false; elements.generateBtn.innerHTML = `${this.icons.download}Create Playlist`; } if (elements.controls) { elements.controls.style.display = 'none'; elements.controls.classList.remove('active'); const pauseBtn = elements.controls.querySelector('.M3Unator-control-btn.pause'); const resumeBtn = elements.controls.querySelector('.M3Unator-control-btn.resume'); const cancelBtn = elements.controls.querySelector('.M3Unator-control-btn.cancel'); if (pauseBtn) pauseBtn.style.display = 'none'; if (resumeBtn) resumeBtn.style.display = 'none'; if (cancelBtn) cancelBtn.style.display = 'none'; } if (enableToggles) { if (elements.includeVideo) elements.includeVideo.disabled = false; if (elements.includeAudio) elements.includeAudio.disabled = false; if (elements.recursiveSearch) elements.recursiveSearch.disabled = false; if (elements.currentDepth) elements.currentDepth.disabled = false; if (elements.customDepth) elements.customDepth.disabled = false; if (elements.maxDepth) elements.maxDepth.disabled = elements.customDepth ? !elements.customDepth.checked : true; } if (uiOnly) return; if (isCancelled) { this.state.stats = JSON.parse(JSON.stringify(this.initialStats)); if (!keepLogs) { if (elements.scanLog) { elements.scanLog.innerHTML = ''; elements.scanLog.classList.add('collapsed'); } if (elements.logToggle) { elements.logToggle.classList.remove('active'); } if (this.domElements.stats) { Object.entries(this.domElements.stats).forEach(([key, element]) => { if (element) { element.textContent = '0'; const statContainer = element.closest('.M3Unator-stat'); if (statContainer) { statContainer.style.opacity = '0.5'; if (key === 'depthLevel') { statContainer.dataset.progress = ''; statContainer.title = 'Depth Level: 0'; } } } }); } } } if (elements.recursiveSearch) { elements.recursiveSearch.checked = true; this.state.recursiveSearch = true; this.state.maxDepth = -1; } if (elements.currentDepth) { elements.currentDepth.checked = false; } if (elements.customDepth) { elements.customDepth.checked = false; } if (elements.maxDepth) { elements.maxDepth.disabled = true; elements.maxDepth.value = '1'; } if (elements.depthControls) { elements.depthControls.classList.remove('active'); } } handleError(error, context = '') { let userMessage = 'An error occurred'; let logMessage = error.message; let type = 'error'; switch (true) { case error.name === 'AbortError': userMessage = 'Server not responding, operation timed out'; logMessage = `Timeout: ${context}`; type = 'warning'; break; case error.message.includes('HTTP error'): const status = error.message.match(/\d+/)?.[0]; switch (status) { case '403': userMessage = 'Access denied to this directory'; break; case '404': userMessage = 'Directory or file not found'; break; case '429': userMessage = 'Too many requests, please wait a while'; break; case '500': case '502': case '503': userMessage = 'Server is currently unable to respond, please try again later'; break; default: userMessage = 'Error communicating with server'; } logMessage = `${error.message} (${context})`; break; case error.message.includes('decode'): userMessage = 'Filename or path could not be read'; logMessage = `Decode error: ${context} - ${error.message}`; type = 'warning'; break; case error.message.includes('NetworkError'): userMessage = 'Network connection error, please check your connection'; logMessage = `Network error: ${context}`; break; case error.message.includes('SecurityError'): userMessage = 'Operation not allowed due to security restrictions'; logMessage = `Security error: ${context}`; break; default: userMessage = 'Unexpected error occurred'; logMessage = `${error.name}: ${error.message} (${context})`; } console.error(`[${context}]`, error); this.showToast(userMessage, type); this.addLogEntry(logMessage, type); this.state.stats.errors.total++; } async fetchWithRetry(url, options = {}, retries = 3) { let lastError; for (let i = 0; i < retries; i++) { try { const response = await fetch(url, { ...options, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }, timeout: options.timeout || 30000, signal: options.signal }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const text = await response.text(); return { decodedText: text, status: response.status, ok: response.ok }; } catch (error) { lastError = error; if (error.name === 'AbortError' || error.message.includes('404')) { throw error; // Throw these errors immediately } if (i < retries - 1) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); continue; } } } throw lastError; } sanitizeInput(input) { if (!input || typeof input !== 'string') { return ''; } const sanitized = input .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '') .trim() .replace(/[\x00-\x1F\x7F]/g, '') .replace(/[\u200B-\u200D\uFEFF]/g, '') .replace(/[^\w\s\-_.()[\]{}#@!$%^&+=]/g, ''); if (!sanitized) { return 'playlist'; } if (sanitized.length > 255) { return sanitized.slice(0, 255); } return sanitized; } decodeString(str, type = 'both') { if (!str) return str; try { let decoded = str; if (type === 'html' || type === 'both') { decoded = decoded.replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(///g, "/"); } if (type === 'url' || type === 'both') { try { decoded = decodeURIComponent(decoded); } catch (e) { decoded = decoded.replace(/%([0-9A-F]{2})/gi, (match, hex) => { try { return String.fromCharCode(parseInt(hex, 16)); } catch { return match; } }); } } return decoded; } catch (error) { console.warn('Decode error:', error); return str; } } extractFileInfo(path) { try { const decodedPath = this.decodeString(path); const parts = decodedPath.split('/'); const fileName = parts.pop() || ''; const dirPath = parts.join('/'); return { fileName, dirPath, original: { fileName: path.split('/').pop() || '', dirPath: path.split('/').slice(0, -1).join('/') } }; } catch (error) { this.handleError(error, `Path decode error: ${path}`); const parts = path.split('/'); return { fileName: parts.pop() || '', dirPath: parts.join('/'), original: { fileName: parts.pop() || '', dirPath: parts.join('/') } }; } } normalizeUrl(url) { let normalized = url.replace(/([^:]\/)\/+/g, "$1"); return normalized.endsWith('/') ? normalized : normalized + '/'; } isMediaFile(fileName, type) { const lowerFileName = fileName.toLowerCase(); return type === 'video' ? this.videoFormats.some(ext => lowerFileName.endsWith(ext)) : this.audioFormats.some(ext => lowerFileName.endsWith(ext)); } resetCurrentStats() { this.state.stats.files.video.current = 0; this.state.stats.files.audio.current = 0; } updateFileStats(type) { this.state.stats.files[type].total++; this.state.stats.files[type].current++; } getCurrentStatsText() { const { video, audio } = this.state.stats.files; const details = []; if (video.current > 0) details.push(`${video.current} video`); if (audio.current > 0) details.push(`${audio.current} audio`); return details.join(' and '); } async scanDirectory(url, currentPath = '', depth = 0) { try { this.resetCurrentStats(); if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) { return this.entries; } while (this.state.isPaused && this.state.isGenerating) { await new Promise(resolve => setTimeout(resolve, 100)); } // Encode special characters properly const normalizedUrl = this.normalizeUrl(url).replace(/#/g, '%23') .replace(/\s+/g, '%20') .replace(/\[/g, '%5B') .replace(/\]/g, '%5D') .replace(/'/g, '%27') .replace(/"/g, '%22'); if (depth > this.state.stats.directories.depth) { this.state.stats.directories.depth = depth; } this.state.stats.directories.total++; this.addLogEntry(`Scanning directory (level ${depth}): ${decodeURIComponent(normalizedUrl)}`); if (this.seenUrls.has(normalizedUrl)) { this.addLogEntry(`This directory was previously scanned: ${decodeURIComponent(normalizedUrl)}`); return this.entries; } this.seenUrls.add(normalizedUrl); if (this.seenUrls.size > this.state.maxSeenUrls) { const keepCount = Math.floor(this.state.maxSeenUrls * 0.75); const urlsArray = Array.from(this.seenUrls); const keepUrls = urlsArray.slice(-keepCount); this.seenUrls = new Set(keepUrls); this.addLogEntry( `Cache cleared (${urlsArray.length} -> ${keepUrls.length})`, 'info' ); } let response; try { response = await this.fetchWithRetry(normalizedUrl, { signal: null, // Remove cancel signal timeout: 30000 // 30 second timeout }); } catch (error) { if (error.name === 'AbortError') { this.addLogEntry(`Request cancelled: ${decodeURIComponent(normalizedUrl)}`, 'warning'); } else if (error.message.includes('404')) { this.addLogEntry(`Directory not found: ${decodeURIComponent(normalizedUrl)}`, 'warning'); } else { this.addLogEntry(`Connection error: ${error.message}`, 'error'); } return this.entries; } const html = response.decodedText; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const isLiteSpeed = doc.querySelector('div#table-list') !== null; let hrefs = []; if (isLiteSpeed) { const rows = doc.querySelectorAll('#table-content tr'); rows.forEach(row => { const linkElement = row.querySelector('a'); if (linkElement && !linkElement.textContent.includes('Parent Directory')) { const href = linkElement.getAttribute('href'); if (href) hrefs.push(href); } }); } else { const hrefRegex = /href="([^"]+)"/gi; const matches = html.matchAll(hrefRegex); hrefs = Array.from(matches, m => m[1]).filter(href => href && !href.startsWith('?') && !href.startsWith('/') && href !== '../' && !href.includes('Parent Directory') ); } // Separate directories and files const directories = []; const files = []; for (const href of hrefs) { if (href.endsWith('/')) { directories.push(href); } else { files.push(href); } } // Process files in batches const batchSize = 100; // Increased from 50 to 100 for (let i = 0; i < files.length; i += batchSize) { if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) break; const batch = files.slice(i, i + batchSize); const batchProgress = { total: batch.length, processed: 0, success: 0, errors: 0 }; // Use Set for better performance const processedUrls = new Set(); await Promise.all(batch.map(async href => { try { // Skip if URL was previously processed const fullUrl = new URL(href, normalizedUrl).toString(); if (processedUrls.has(fullUrl)) return; processedUrls.add(fullUrl); const decodedHref = this.decodeString(href); const { fileName } = this.extractFileInfo(decodedHref); const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName; this.state.stats.totalFiles = (this.state.stats.totalFiles || 0) + 1; // Check file type using Map const mediaType = this.isMediaFileOptimized(fileName); if (mediaType && ((mediaType === 'video' && this.state.includeVideo) || (mediaType === 'audio' && this.state.includeAudio))) { if (mediaType === 'video') { this.updateFileStats('video'); } else { this.updateFileStats('audio'); } this.entries.push({ title: fullPath, url: fullUrl }); batchProgress.success++; } } catch (error) { console.error('URL processing error:', error); this.state.stats.errors.total++; batchProgress.errors++; } finally { batchProgress.processed++; // Update progress every 20 operations (instead of 10) if (batchProgress.processed % 20 === 0 || batchProgress.processed === batchProgress.total) { const progress = Math.floor((batchProgress.processed / batchProgress.total) * 100); this.addLogEntry( `Batch Processing: ${progress}% (${batchProgress.processed}/${batchProgress.total}, ` + `Success: ${batchProgress.success}, Error: ${batchProgress.errors})`, 'info' ); } } })); // Increase interval for memory cleanup if (i > 0 && i % (batchSize * 20) === 0) { global.gc && global.gc(); } } // Scan directories in parallel const shouldScanSubdir = this.state.maxDepth === -1 || (this.state.maxDepth > 0 && depth < this.state.maxDepth); if (shouldScanSubdir && directories.length > 0) { const parallelLimit = 15; // Parallel limit increased to 15 const queue = [...directories]; const activeRequests = new Set(); while (queue.length > 0 || activeRequests.size > 0) { // Start new request if there are items in queue and active request limit is not reached while (queue.length > 0 && activeRequests.size < parallelLimit) { const dir = queue.shift(); try { const decodedDir = this.decodeString(dir); const fullUrl = new URL(decodedDir, normalizedUrl).toString(); const { fileName } = this.extractFileInfo(decodedDir); const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName; const promise = this.scanDirectory(fullUrl, fullPath, depth + 1) .finally(() => { activeRequests.delete(promise); }); activeRequests.add(promise); this.addLogEntry(`Entering subdirectory: ${fullPath}`); } catch (error) { console.error('Directory scanning error:', error); this.state.stats.errors.total++; } } // Wait for one of the active requests to complete if (activeRequests.size > 0) { await Promise.race(activeRequests); } } } this.updateCounter(this.state.stats.totalFiles); return this.entries; } catch (error) { this.state.stats.errors.total++; this.addLogEntry(`Scan error (${currentPath || url}): ${error.message}`, 'error'); return this.entries; } } createPlaylist(entries) { let content = '#EXTM3U\n'; const decodedEntries = entries.map(entry => { try { let title = this.decodeString(entry.title); const depth = (title.match(/\//g) || []).length; const isVideo = this.videoFormats.some(ext => title.toLowerCase().endsWith(ext)); const isAudio = this.audioFormats.some(ext => title.toLowerCase().endsWith(ext)); return { ...entry, decodedTitle: title, depth: depth, isVideo: isVideo, isAudio: isAudio }; } catch (error) { return { ...entry, decodedTitle: entry.title, depth: 0, isVideo: false, isAudio: false }; } }); const videoEntries = decodedEntries.filter(entry => entry.isVideo); const audioEntries = decodedEntries.filter(entry => entry.isAudio); const apacheSort = (a, b) => { if (a.depth !== b.depth) { return a.depth - b.depth; } const aStartsWithNumber = /^\d/.test(a.decodedTitle); const bStartsWithNumber = /^\d/.test(b.decodedTitle); if (aStartsWithNumber !== bStartsWithNumber) { return aStartsWithNumber ? -1 : 1; } return a.decodedTitle.localeCompare(b.decodedTitle, undefined, { numeric: true, sensitivity: 'base' }); }; const sortedVideoEntries = videoEntries.sort(apacheSort); const sortedAudioEntries = audioEntries.sort(apacheSort); const sortedEntries = [...sortedVideoEntries, ...sortedAudioEntries]; sortedEntries.forEach(entry => { content += `#EXTINF:-1,${entry.decodedTitle}\n${entry.url}\n`; }); return content; } addLogEntry(message, type = '') { if ((this.state.isPaused || !this.state.isGenerating) && type !== 'final') { return; } let decodedMessage = message; try { if (message.includes('http')) { const urlRegex = /(https?:\/\/[^\s]+)/g; decodedMessage = message.replace(urlRegex, (url) => { try { return decodeURIComponent(url); } catch (e) { return url; } }); } } catch (error) { console.warn('Decode error:', error); } // Add only to cache this.logCache.add(decodedMessage, type); // Update UI every 10 logs if (this.logCache.stats.totalLogs % 10 === 0) { this.updateLogUI(); } } updateLogUI() { const scanLog = this.domElements.scanLog; if (!scanLog) return; // Add throttle for performance if (this._updateLogUITimeout) { clearTimeout(this._updateLogUITimeout); } this._updateLogUITimeout = setTimeout(() => { requestAnimationFrame(() => { const wasAtBottom = Math.abs(scanLog.scrollHeight - scanLog.clientHeight - scanLog.scrollTop) < 50; // Use fragment to minimize DOM manipulation const fragment = document.createDocumentFragment(); // Show last 50 logs (instead of 100) const recentLogs = this.logCache.logs.slice(-50); recentLogs.forEach(log => { const div = document.createElement('div'); div.className = `M3Unator-log-entry ${log.type}`; div.innerHTML = ` ${log.timestamp} ${log.message} `; fragment.appendChild(div); }); scanLog.innerHTML = ''; scanLog.appendChild(fragment); if (wasAtBottom) { scanLog.scrollTop = scanLog.scrollHeight; } }); }, 100); // 100ms throttle } generateScanReport() { const stats = this.state.stats; const logCache = this.logCache; const summary = [ `📊 Scan Summary`, `───────────────`, `📁 Total Files: ${stats.totalFiles}`, `🎥 Video Files: ${stats.files.video.total}`, `🎵 Audio Files: ${stats.files.audio.total}`, `📂 Directories: ${stats.directories.total}`, `↕️ Maximum Depth: ${stats.directories.depth}`, stats.errors.total > 0 ? `⚠️ Errors: ${stats.errors.total} (${stats.errors.skipped} skipped)` : null, ``, `📝 Log Statistics`, `───────────────`, `Total Logs: ${logCache.stats.totalLogs}`, logCache.stats.skippedLogs > 0 ? `Skipped Logs: ${logCache.stats.skippedLogs}` : null, ``, `🔍 Last ${logCache.maxSize} Log Entries`, `───────────────`, ...logCache.logs.map(log => `[${log.timestamp}] ${log.type === 'error' ? '❌' : log.type === 'warning' ? '⚠️' : log.type === 'success' ? '✅' : 'ℹ️'} ${log.message}`) ].filter(Boolean).join('\n'); return summary; } updateCounter(count) { if (!this.domElements.stats || !this.domElements.statsBar) { return; } const stats = this.state.stats; const elements = this.domElements.stats; const statsBar = this.domElements.statsBar; statsBar.style.display = 'block'; const updates = { 'totalFiles': count, 'videoFiles': stats.files.video.total, 'audioFiles': stats.files.audio.total, 'directories': stats.directories.total, 'depthLevel': stats.directories.depth, 'errors': stats.errors.total }; Object.entries(updates).forEach(([key, value]) => { const element = elements[key]; if (element) { element.textContent = value; const statContainer = element.closest('.M3Unator-stat'); if (statContainer) { statContainer.style.opacity = value > 0 ? '1' : '0.5'; if (key === 'depthLevel') { const maxDepth = this.state.maxDepth || 0; if (maxDepth > 0) { const progress = (value / maxDepth) * 100; statContainer.dataset.progress = progress >= 100 ? 'high' : progress >= 75 ? 'medium' : progress >= 50 ? 'low' : ''; statContainer.title = `Depth Level: ${value}/${maxDepth}`; } else { statContainer.dataset.progress = ''; statContainer.title = `Depth Level: ${value}`; } } } } }); } // Add new method for file type checking isMediaFileOptimized(fileName) { const extension = fileName.toLowerCase().split('.').pop(); return this.extensionMap.get(extension); } } const generator = new PlaylistGenerator(); generator.init(); // Event listeners for info modal document.querySelector('.info-link').addEventListener('click', () => { document.querySelector('.info-modal').style.display = 'block'; document.body.classList.add('modal-open'); }); document.querySelector('.info-close').addEventListener('click', () => { document.querySelector('.info-modal').style.display = 'none'; document.body.classList.remove('modal-open'); }); window.addEventListener('click', (event) => { const modal = document.querySelector('.info-modal'); if (event.target === modal) { modal.style.display = 'none'; document.body.classList.remove('modal-open'); } }); // Event listener for playlist name input generator.domElements.playlistInput.addEventListener('input', (e) => { const dropdown = e.target.parentElement.querySelector('.M3Unator-dropdown'); if (e.target.value.trim()) { dropdown.style.display = 'block'; } else { dropdown.style.display = 'none'; } }); })();