您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Радио на 4PDA с поиском через API и флагами стран
// ==UserScript== // @name 4PDA Radio v1.14 // @author brant34 // @namespace http://tampermonkey.net/ // @version 1.14 // @description Радио на 4PDA с поиском через API и флагами стран // @match https://4pda.to/forum/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; // Проверка поддержки localStorage if (!window.localStorage) { showNotification('localStorage недоступен', 'error'); return; } // === [Стили] === GM_addStyle(` .radio-toggle-button { position: fixed; top: 10px; right: 10px; background-color: #2e6d5e; color: #fff; border: none; border-radius: 50%; width: 32px; height: 32px; cursor: pointer; font-size: 16px; line-height: 32px; text-align: center; z-index: 99999; } .radio-toggle-button:hover { background-color: #3e8e77; } .radio-panel { display: none; background-color: #1a3c34; border-radius: 10px; padding: 10px; z-index: 99998; color: #fff; font-family: Arial, sans-serif; font-size: 14px; width: 320px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); position: fixed; max-width: 90vw; } .radio-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .radio-panel-header span { font-weight: bold; } .radio-panel-controls { display: flex; flex-wrap: wrap; gap: 5px; } .radio-panel-controls button { background-color: #17a2b8; color: #fff; border: none; border-radius: 5px; padding: 5px 10px; cursor: pointer; font-size: 12px; } .radio-panel-controls button:hover { background-color: #138496; } .radio-panel select { background-color: #fff; border: 1px solid #ccc; border-radius: 5px; padding: 5px; color: #333; font-size: 12px; flex-grow: 1; } .radio-player { background-color: transparent; border-radius: 5px; padding: 5px; margin: 10px 0; display: flex; align-items: center; gap: 5px; } .radio-player button { background: none; border: none; cursor: pointer; font-size: 16px; color: #fff; } .radio-player input[type="range"] { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; width: 100%; } .radio-player input[type="range"]::-webkit-slider-runnable-track { background: #2e6d5e; height: 6px; border-radius: 3px; } .radio-player input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; background: #fff; height: 16px; width: 16px; border-radius: 50%; margin-top: -5px; } .radio-player input[type="range"]::-moz-range-track { background: #2e6d5e; height: 6px; border-radius: 3px; } .radio-player input[type="range"]::-moz-range-thumb { background: #fff; height: 16px; width: 16px; border-radius: 50%; border: none; } .radio-player input[type="range"]::-moz-range-progress { background: #fff; height: 6px; border-radius: 3px; } .radio-player input[type="range"]:disabled::-webkit-slider-runnable-track { background: #1a3c34; opacity: 0.7; } .radio-player input[type="range"]:disabled::-webkit-slider-thumb { background: #999; } .radio-player input[type="range"]:disabled::-moz-range-track { background: #1a3c34; opacity: 0.7; } .radio-player input[type="range"]:disabled::-moz-range-thumb { background: #999; } .radio-player input[type="range"]:disabled::-moz-range-progress { background: #999; } .radio-player .volume-icon { cursor: pointer; } .radio-player .volume-icon.muted::before { content: "🔇"; } .radio-player .volume-icon:not(.muted)::before { content: "🔊"; } .radio-panel input[type="checkbox"] { margin-right: 5px; } .radio-panel-settings { margin-top: 10px; display: flex; gap: 5px; } .radio-search { display: flex; gap: 5px; margin: 10px 0; } .radio-search input[type="text"] { flex-grow: 1; padding: 5px; border-radius: 5px; border: 1px solid #ccc; background-color: #fff; color: #333; font-size: 12px; } .radio-search button { background-color: #17a2b8; color: #fff; border: none; border-radius: 5px; padding: 5px 10px; cursor: pointer; font-size: 12px; } .radio-search button:hover { background-color: #138496; } .radio-search-results { max-height: 150px; overflow-y: auto; margin-top: 5px; padding: 5px; background-color: #2e6d5e; border-radius: 5px; } .radio-search-results div { display: flex; justify-content: space-between; align-items: center; padding: 5px; border-bottom: 1px solid #1a3c34; } .radio-search-results div:last-child { border-bottom: none; } .radio-search-results button { background-color: #17a2b8; color: #fff; border: none; border-radius: 5px; padding: 3px 8px; cursor: pointer; font-size: 10px; } .radio-search-results button:hover { background-color: #138496; } .notification { position: fixed; top: 50px; right: 10px; padding: 10px 20px; border-radius: 5px; color: white; z-index: 99999; } .notification.success { background-color: #28a745; } .notification.info { background-color: #17a2b8; } .notification.warning { background-color: #ffc107; } .notification.error { background-color: #dc3545; } `); // === [Эксклюзивное воспроизведение радио во вкладке] === const tabId = Date.now().toString(); const MASTER_KEY = '4pda-radio-master'; function setAsMaster() { localStorage.setItem(MASTER_KEY, tabId); showNotification('Эта вкладка теперь воспроизводит радио', 'success'); } function isMaster() { return localStorage.getItem(MASTER_KEY) === tabId; } // === [Инициализация аудиоплеера] === let audio = document.getElementById('radioPlayer4PDA'); if (!audio) { audio = document.createElement('audio'); audio.id = 'radioPlayer4PDA'; document.body.appendChild(audio); } // Проверка перед запуском радио const currentMaster = localStorage.getItem(MASTER_KEY); if (!currentMaster) { setAsMaster(); } else if (!isMaster()) { audio.pause(); } // Слушаем изменения мастера window.addEventListener('storage', (e) => { if (e.key === MASTER_KEY && e.newValue !== tabId) { audio.pause(); } }); // Убираем себя из мастеров при закрытии вкладки window.addEventListener('beforeunload', () => { if (isMaster()) { localStorage.removeItem(MASTER_KEY); } }); // === [Сохраненные настройки] === const savedAutoplay = GM_getValue('autoplay', false); const savedRadio = GM_getValue('radio', ''); const savedVolume = GM_getValue('volume', 1); const savedTimer = GM_getValue('autotimer', 0); const savedPlaying = GM_getValue('isPlaying', false); const savedTime = GM_getValue('currentTime', 0); let panelPosition = GM_getValue('panelPos', 'top-right'); const panelScale = GM_getValue('panelSize', '1'); const savedCustomStations = GM_getValue('customStations', {}); // === [Список радиостанций] === let RADIO = { '🇷🇺 Европа Плюс': 'https://ep256.hostingradio.ru:8052/europaplus256.mp3', '🇷🇺 Русское Радио': 'https://rusradio.hostingradio.ru/rusradio128.mp3', '🇷🇺 Юмор FM': 'https://pub0301.101.ru:8443/stream/air/mp3/256/102', '🇷🇺 Радио Рекорд': 'https://radio-srv1.11one.ru/record192k.mp3', '🇷🇺 Ретро FM': 'https://retro.hostingradio.ru:8014/retro320.mp3', '🇷🇺 Радио Шансон': 'https://chanson.hostingradio.ru:8041/chanson256.mp3', '🇷🇺 DFM Russian Dance': 'https://stream03.pcradio.ru/dfm_russian_dance-hi', '🇷🇺 DFM': 'https://dfm.hostingradio.ru:80/dfm96.aacp', '🇷🇺 Дорожное Радио': 'https://dorognoe.hostingradio.ru:8000/dorognoe', '🇷🇺 Авторадио': 'https://srv01.gpmradio.ru/stream/air/aac/64/100?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiIwZWM3MjU3YTFhNDM5MmMyNWUwZDZkZDQwYjdjNzQ5ZCIsIklQIjoiODEuMTczLjE2NS4yMjUiLCJVQSI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzMuMC4wLjAgU2FmYXJpLzUzNy4zNiIsIlJlZiI6Imh0dHBzOi8vd3d3LmF2dG9yYWRpby5ydS8iLCJ1aWRfY2hhbm5lbCI6IjEwMCIsInR5cGVfY2hhbm5lbCI6ImNoYW5uZWwiLCJ0eXBlRGV2aWNlIjoiUEMiLCJCcm93c2VyIjoiQ2hyb21lIiwiQnJvd3NlclZlcnNpb24iOiIxMzMuMC4wLjAiLCJTeXN0ZW0iOiJNYWMgT1MgWCBQdW1hIiwiZXhwIjoxNzQyNjcxOTc1fQ.b1Hha0aGp4hWbgFELSzEapRcpOoejzs8tmdDARY0JyA', '🇩🇪 Радио Картина': 'https://rs.kartina.tv/kartina_320kb', '🇰🇿 LuxFM': 'https://icecast.luxfm.kz/luxfm', '🇰🇿 Radio NS': 'https://icecast.ns.kz/radions', '🇰🇿 NRJ Kazakhstan': 'https://stream03.pcradio.ru/energyfm_ru-med', '🇰🇿 Радио Жаңа FM': 'https://live.zhanafm.kz:8443/zhanafm_onair', '🇺🇦 Хіт FM': 'http://online.hitfm.ua/HitFM', '🇺🇦 Kiss FM UA': 'http://online.kissfm.ua/KissFM' }; // Объединяем предопределенные станции с пользовательскими Object.assign(RADIO, savedCustomStations); // === [Проверка доступности радиопотоков] === async function checkStream(url) { return true; // Заглушка, можно добавить реальную проверку } async function validateStations() { const validStations = {}; for (const [name, url] of Object.entries(RADIO)) { if (await checkStream(url)) { validStations[name] = url; } else { showNotification(`Радиостанция ${name} недоступна`, 'warning'); } } RADIO = validStations; updateStationList(); } // === [Динамическое обновление списка радиостанций] === async function loadStations() { try { const response = await new Promise((resolve) => { setTimeout(() => resolve({ ok: true, json: () => Promise.resolve(RADIO) }), 1000); }); if (response.ok) { RADIO = await response.json(); await validateStations(); showNotification('Список радиостанций обновлен', 'success'); } else { showNotification('Ошибка загрузки списка радиостанций', 'error'); } } catch (error) { console.error('Ошибка обновления радиостанций:', error); showNotification('Ошибка обновления радиостанций', 'error'); } } // === [Уведомления] === function showNotification(message, type) { const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => notification.remove(), 3000); } // === [Преобразование кода страны в флаг] === function countryCodeToFlag(countryCode) { if (!countryCode || countryCode.length !== 2) { return '🌐'; // Нейтральный флаг, если код страны отсутствует } const codePoints = countryCode .toUpperCase() .split('') .map(char => 0x1F1E6 + (char.charCodeAt(0) - 65)); // 'A' → 0x1F1E6, 'B' → 0x1F1E7, ..., 'U' → 0x1F1FA return String.fromCodePoint(...codePoints); } // === [Поиск через RadioBrowser API] === function searchStations(query, callback) { showNotification('Поиск...', 'info'); GM_xmlhttpRequest({ method: 'GET', url: `https://de1.api.radio-browser.info/json/stations/search?name=${encodeURIComponent(query)}&limit=10`, onload: function(response) { try { const data = JSON.parse(response.responseText); const results = data.map(station => ({ name: station.name, url: station.url_resolved, countryCode: station.countrycode || '' })); callback(results); } catch (error) { console.error('Ошибка парсинга ответа API:', error); showNotification('Ошибка поиска радиостанций', 'error'); callback([]); } }, onerror: function(error) { console.error('Ошибка запроса к API:', error); showNotification('Ошибка поиска радиостанций', 'error'); callback([]); } }); } function addStation(name, url, countryCode) { // Добавляем флаг к имени станции const flag = countryCodeToFlag(countryCode); const stationNameWithFlag = `${flag} ${name}`; if (RADIO[stationNameWithFlag]) { showNotification('Радиостанция уже добавлена', 'warning'); return; } RADIO[stationNameWithFlag] = url; const customStations = GM_getValue('customStations', {}); customStations[stationNameWithFlag] = url; GM_setValue('customStations', customStations); updateStationList(); showNotification(`Радиостанция ${stationNameWithFlag} добавлена`, 'success'); } // === [Интерфейс] === function createInterface() { // Панель радио const panel = document.createElement('div'); panel.className = 'radio-panel'; panel.style.display = GM_getValue('panelVisible', false) ? 'block' : 'none'; // Кнопка S const toggleButton = document.createElement('button'); toggleButton.className = 'radio-toggle-button'; toggleButton.textContent = '🎧'; toggleButton.onclick = () => { if (!panel) { console.error('Панель не найдена'); showNotification('Ошибка: панель не создана', 'error'); return; } panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; GM_setValue('panelVisible', panel.style.display === 'block'); showNotification(`Панель ${panel.style.display === 'block' ? 'открыта' : 'закрыта'}`, 'info'); }; document.body.appendChild(toggleButton); // Применение позиции и масштаба updatePanelPosition(panel, panelPosition); updatePanelScale(panel, panelScale); // Заголовок const header = document.createElement('div'); header.className = 'radio-panel-header'; header.innerHTML = '<span>⚡ Громкость:</span>'; panel.appendChild(header); // Кнопки пресетов громкости const controls = document.createElement('div'); controls.className = 'radio-panel-controls'; ['Тихо', 'Комфорт', 'Громко'].forEach((label, index) => { const button = document.createElement('button'); button.textContent = label; button.onclick = () => { if (!audio) { console.error('Аудиоплеер не инициализирован'); showNotification('Ошибка: аудиоплеер не доступен', 'error'); return; } const volumes = [0.2, 0.5, 0.8]; audio.volume = volumes[index]; GM_setValue('volume', audio.volume); updateVolumeSlider(audio.volume); showNotification(`Громкость: ${label} (${volumes[index] * 100}%)`, 'info'); }; controls.appendChild(button); }); panel.appendChild(controls); // Ползунок громкости const volumeSlider = document.createElement('input'); volumeSlider.type = 'range'; volumeSlider.min = '0'; volumeSlider.max = '1'; volumeSlider.step = '0.01'; volumeSlider.value = savedVolume; volumeSlider.oninput = () => { if (!audio) { console.error('Аудиоплеер не инициализирован'); showNotification('Ошибка: аудиоплеер не доступен', 'error'); return; } audio.volume = volumeSlider.value; GM_setValue('volume', audio.volume); }; controls.appendChild(volumeSlider); // Выбор радиостанции const stationSelect = document.createElement('select'); stationSelect.id = 'radioStationSelect'; updateStationList(); controls.appendChild(stationSelect); panel.appendChild(controls); // Поиск радиостанций const searchSection = document.createElement('div'); searchSection.className = 'radio-search'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Поиск радиостанций...'; searchSection.appendChild(searchInput); const searchButton = document.createElement('button'); searchButton.textContent = 'Поиск'; searchSection.appendChild(searchButton); panel.appendChild(searchSection); // Результаты поиска const searchResults = document.createElement('div'); searchResults.className = 'radio-search-results'; searchResults.style.display = 'none'; panel.appendChild(searchResults); searchButton.onclick = () => { const query = searchInput.value.trim(); if (!query) { showNotification('Введите запрос для поиска', 'warning'); return; } searchStations(query, (results) => { searchResults.innerHTML = ''; searchResults.style.display = results.length ? 'block' : 'none'; if (!results.length) { showNotification('Радиостанции не найдены', 'info'); return; } results.forEach(station => { const resultItem = document.createElement('div'); const flag = countryCodeToFlag(station.countryCode); resultItem.textContent = `${flag} ${station.name}`; const addButton = document.createElement('button'); addButton.textContent = 'Добавить'; addButton.onclick = () => { addStation(station.name, station.url, station.countryCode); searchResults.style.display = 'none'; searchInput.value = ''; }; resultItem.appendChild(addButton); searchResults.appendChild(resultItem); }); }); }; // Плеер const player = document.createElement('div'); player.className = 'radio-player'; const playButton = document.createElement('button'); playButton.textContent = savedPlaying ? '⏸' : '▶'; playButton.onclick = togglePlay; player.appendChild(playButton); const timeSlider = document.createElement('input'); timeSlider.type = 'range'; timeSlider.min = '0'; timeSlider.max = '100'; timeSlider.value = '0'; timeSlider.disabled = true; player.appendChild(timeSlider); const timeDisplay = document.createElement('span'); timeDisplay.textContent = '0:00'; player.appendChild(timeDisplay); const volumeIcon = document.createElement('span'); volumeIcon.className = 'volume-icon'; volumeIcon.onclick = () => { if (!audio) { console.error('Аудиоплеер не инициализирован'); showNotification('Ошибка: аудиоплеер не доступен', 'error'); return; } audio.muted = !audio.muted; volumeIcon.classList.toggle('muted', audio.muted); showNotification(audio.muted ? 'Звук выключен' : 'Звук включен', 'info'); }; player.appendChild(volumeIcon); panel.appendChild(player); // Таймер, автостарт и обновление const footer = document.createElement('div'); footer.className = 'radio-panel-controls'; const timerSelect = document.createElement('select'); timerSelect.innerHTML = ` <option value="0">Без таймера</option> <option value="10">10 мин</option> <option value="30">30 мин</option> <option value="60">60 мин</option> `; timerSelect.value = savedTimer; timerSelect.onchange = () => { GM_setValue('autotimer', parseInt(timerSelect.value) || 0); setAutoTimer(parseInt(timerSelect.value) || 0); }; footer.appendChild(timerSelect); const autostartLabel = document.createElement('label'); const autostartCheckbox = document.createElement('input'); autostartCheckbox.type = 'checkbox'; autostartCheckbox.checked = savedAutoplay; autostartCheckbox.onchange = () => { GM_setValue('autoplay', autostartCheckbox.checked); }; autostartLabel.appendChild(autostartCheckbox); autostartLabel.appendChild(document.createTextNode('Автостарт')); footer.appendChild(autostartLabel); const refreshButton = document.createElement('button'); refreshButton.textContent = '↻'; refreshButton.title = 'Обновить станции'; refreshButton.onclick = loadStations; footer.appendChild(refreshButton); panel.appendChild(footer); // Настройки панели const settings = document.createElement('div'); settings.className = 'radio-panel-settings'; const positionSelect = document.createElement('select'); positionSelect.innerHTML = ` <option value="top-left">Вверху слева</option> <option value="top-center">Вверху посередине</option> <option value="top-right">Вверху справа</option> `; positionSelect.value = panelPosition; positionSelect.onchange = () => { GM_setValue('panelPos', positionSelect.value); panelPosition = positionSelect.value; updatePanelPosition(panel, positionSelect.value); showNotification(`Панель перемещена: ${positionSelect.options[positionSelect.selectedIndex].text}`, 'info'); }; settings.appendChild(positionSelect); const scaleSelect = document.createElement('select'); scaleSelect.innerHTML = ` <option value="0.8">Маленький</option> <option value="1">Средний</option> <option value="1.1">Большой</option> `; scaleSelect.value = panelScale; scaleSelect.onchange = () => { GM_setValue('panelSize', scaleSelect.value); updatePanelScale(panel, scaleSelect.value); showNotification(`Масштаб панели: ${scaleSelect.options[scaleSelect.selectedIndex].text}`, 'info'); }; settings.appendChild(scaleSelect); panel.appendChild(settings); document.body.appendChild(panel); // Функция для обновления ползунка громкости function updateVolumeSlider(value) { volumeSlider.value = value; } } function updatePanelPosition(panel, position) { if (!panel) { console.error('Панель не найдена для обновления позиции'); return; } panel.style.top = '10px'; panel.style.bottom = ''; panel.style.left = ''; panel.style.right = ''; panel.style.transform = ''; switch (position) { case 'top-left': panel.style.left = '10px'; break; case 'top-center': panel.style.left = '50%'; panel.style.transform = 'translateX(-50%)'; break; case 'top-right': panel.style.right = '10px'; break; } } function updatePanelScale(panel, scale) { if (!panel) { console.error('Панель не найдена для обновления масштаба'); return; } panel.style.transform = `scale(${scale})`; panel.style.transformOrigin = panelPosition.includes('left') ? 'top left' : panelPosition.includes('right') ? 'top right' : 'top center'; if (parseFloat(scale) > 1) { panel.style.maxWidth = '80vw'; if (panelPosition === 'top-center') { panel.style.left = '50%'; panel.style.transform = `translateX(-50%) scale(${scale})`; } } else { panel.style.maxWidth = '90vw'; } } function updateStationList() { const stationSelect = document.getElementById('radioStationSelect'); if (!stationSelect) { return; } stationSelect.innerHTML = '<option value="">Выберите радиостанцию</option>'; Object.keys(RADIO).forEach(name => { const option = document.createElement('option'); option.value = RADIO[name]; option.textContent = name; if (RADIO[name] === savedRadio) option.selected = true; stationSelect.appendChild(option); }); stationSelect.onchange = () => { if (stationSelect.value) { audio.src = stationSelect.value; GM_setValue('radio', stationSelect.value); if (savedAutoplay || savedPlaying) { audio.play().catch(e => { console.error('Ошибка воспроизведения:', e); showNotification('Ошибка воспроизведения радиостанции', 'error'); }); } } }; } // === [Управление воспроизведением] === function togglePlay() { const playButton = document.querySelector('.radio-player button'); if (!playButton) { return; } if (audio.paused) { if (isMaster()) { audio.play().catch(e => { console.error('Ошибка воспроизведения:', e); showNotification('Ошибка воспроизведения радиостанции', 'error'); }); GM_setValue('isPlaying', true); playButton.textContent = '⏸'; } } else { audio.pause(); GM_setValue('isPlaying', false); playButton.textContent = '▶'; } } // === [Таймер автовыключения] === let timerId; function setAutoTimer(minutes) { clearTimeout(timerId); if (minutes > 0) { timerId = setTimeout(() => { audio.pause(); GM_setValue('isPlaying', false); const playButton = document.querySelector('.radio-player button'); if (playButton) { playButton.textContent = '▶'; } showNotification('Радио остановлено по таймеру', 'info'); }, minutes * 60 * 1000); } } // === [Инициализация] === try { createInterface(); // Инициализация аудио audio.volume = savedVolume; if (savedRadio) { audio.src = savedRadio; if (savedAutoplay && isMaster()) { audio.play().catch(e => { console.error('Ошибка воспроизведения:', e); showNotification('Ошибка воспроизведения радиостанции', 'error'); }); } } audio.ontimeupdate = () => GM_setValue('currentTime', audio.currentTime); audio.onerror = () => { console.error('Ошибка загрузки радиопотока'); showNotification('Ошибка загрузки радиопотока', 'error'); }; // Обновление списка радиостанций без проверки updateStationList(); setAutoTimer(savedTimer); } catch (error) { console.error('Критическая ошибка инициализации:', error); showNotification('Ошибка запуска скрипта', 'error'); } })();