New:Socket for Asana Is Now Available.Learn more
Get Started

YouTube Recorder Pro

Package Overview
Versions
1
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

yt-recorder-unique-id@example.com - firefox Package Compare versions

Comparing version
2.8
to
3.4
+18
-2
background.js
browser.runtime.onInstalled.addListener(() => {
console.log("YouTube Recorder установлен");
});
console.log("YouTube Recorder Pro установлен");
});
// Обработка горячих клавиш (если поддерживается)
if (typeof browser.commands !== 'undefined') {
browser.commands.onCommand.addListener(async (command) => {
if (command === "toggle-recording") {
const tabs = await browser.tabs.query({active: true, currentWindow: true});
if (tabs[0].url.includes('youtube.com')) {
try {
await browser.tabs.sendMessage(tabs[0].id, {action: "toggle_recording"});
} catch (e) {
console.error("Content script not loaded:", e);
}
}
}
});
}
+446
-75

@@ -1,97 +0,348 @@

let recorder;
let recordedChunks = [];
let indicator;
let audioContext;
let streamSource;
// Проверка, что скрипт уже загружен
if (window.ytRecorderLoaded) {
console.log('YouTube Recorder already loaded');
} else {
window.ytRecorderLoaded = true;
// Функция для управления мигающей красной точкой
function toggleIndicator(show) {
if (show) {
if (document.getElementById('yt-recorder-indicator')) return;
indicator = document.createElement('div');
indicator.id = 'yt-recorder-indicator';
Object.assign(indicator.style, {
let recorder;
let recordedChunks = [];
let indicator;
let audioContext;
let originalVideoTitle = '';
let recordingSettings = {};
let stopTimeout;
let mediaStream = null;
let checkTimeInterval = null;
let stopButton = null;
let audioDestination = null;
// Функция для управления мигающей красной точкой
function toggleIndicator(show) {
if (show) {
if (document.getElementById('yt-recorder-indicator')) return;
indicator = document.createElement('div');
indicator.id = 'yt-recorder-indicator';
Object.assign(indicator.style, {
position: 'fixed',
top: '20px',
right: '20px',
width: '16px',
height: '16px',
backgroundColor: 'red',
borderRadius: '50%',
zIndex: '2147483647',
boxShadow: '0 0 10px rgba(0,0,0,0.7)',
pointerEvents: 'none',
border: '2px solid white'
});
indicator.animate([
{ opacity: 1 }, { opacity: 0.2 }, { opacity: 1 }
], {
duration: 1000,
iterations: Infinity
});
const label = document.createElement('div');
label.textContent = 'REC';
Object.assign(label.style, {
position: 'fixed',
top: '18px',
right: '40px',
color: 'red',
fontWeight: 'bold',
fontFamily: 'monospace',
fontSize: '12px',
zIndex: '2147483647',
pointerEvents: 'none'
});
label.id = 'yt-recorder-label';
document.body.appendChild(indicator);
document.body.appendChild(label);
createStopButton();
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
} else {
const existingIndicator = document.getElementById('yt-recorder-indicator');
const existingLabel = document.getElementById('yt-recorder-label');
if (existingIndicator) existingIndicator.remove();
if (existingLabel) existingLabel.remove();
indicator = null;
removeStopButton();
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
}
}
function createStopButton() {
if (document.getElementById('yt-recorder-stop-btn')) return;
stopButton = document.createElement('button');
stopButton.id = 'yt-recorder-stop-btn';
stopButton.innerHTML = '⏹ Остановить запись';
Object.assign(stopButton.style, {
position: 'fixed',
top: '20px',
right: '20px',
width: '16px',
height: '16px',
backgroundColor: 'red',
borderRadius: '50%',
right: '80px',
padding: '8px 16px',
backgroundColor: '#cc0000',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '13px',
fontWeight: 'bold',
cursor: 'pointer',
zIndex: '2147483647',
boxShadow: '0 0 10px rgba(0,0,0,0.7)',
pointerEvents: 'none',
border: '2px solid white'
boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
});
indicator.animate([
{ opacity: 1 }, { opacity: 0.2 }, { opacity: 1 }
], {
duration: 1000,
iterations: Infinity
stopButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
stopRecording();
});
document.body.appendChild(stopButton);
}
document.body.appendChild(indicator);
} else {
const existingIndicator = document.getElementById('yt-recorder-indicator');
if (existingIndicator) {
existingIndicator.remove();
indicator = null;
function removeStopButton() {
const btn = document.getElementById('yt-recorder-stop-btn');
if (btn) btn.remove();
stopButton = null;
}
function handleFullscreenChange() {
if (!recorder || recorder.state === "inactive") return;
const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
const indicator = document.getElementById('yt-recorder-indicator');
const label = document.getElementById('yt-recorder-label');
const stopBtn = document.getElementById('yt-recorder-stop-btn');
if (isFullscreen) {
const fsElement = document.fullscreenElement || document.webkitFullscreenElement;
if (indicator && !fsElement.contains(indicator)) {
fsElement.appendChild(indicator);
fsElement.appendChild(label);
if (stopBtn) fsElement.appendChild(stopBtn);
}
} else {
if (indicator && !document.body.contains(indicator)) {
document.body.appendChild(indicator);
document.body.appendChild(label);
if (stopBtn) document.body.appendChild(stopBtn);
}
}
}
}
// Обработчик сообщений (используем browser для Firefox)
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "start_recording") {
const video = document.querySelector('video');
function getVideoTitle() {
// Для обычных видео
const titleElement = document.querySelector('h1.title.style-scope.ytd-video-primary-info-renderer yt-formatted-string, h1.ytd-watch-metadata yt-formatted-string, #title h1, h1.style-scope.ytd-watch-metadata');
if (titleElement) {
return titleElement.textContent.trim().replace(/[<>":/\\|?*]/g, '_').substring(0, 100);
}
// Для Shorts
const shortsTitle = document.querySelector('h2.style-scope.ytd-reel-player-header-renderer yt-formatted-string, .ytd-reel-player-overlay-renderer h2 yt-formatted-string, [class*="shorts"] h2');
if (shortsTitle) {
return shortsTitle.textContent.trim().replace(/[<>":/\\|?*]/g, '_').substring(0, 100);
}
// Заголовок из meta
const metaTitle = document.querySelector('meta[property="og:title"]');
if (metaTitle) {
return metaTitle.content.trim().replace(/[<>":/\\|?*]/g, '_').substring(0, 100);
}
return 'youtube-video-' + Date.now();
}
function getVideoElement() {
// Сначала ищем активное/видимое видео
const videos = document.querySelectorAll('video');
for (let video of videos) {
// Проверяем что видео готово и имеет размеры
if (video.readyState >= 2 && video.videoWidth > 0) {
// Для Shorts - проверяем что видео в viewport или активно
if (recordingSettings.isShorts) {
const rect = video.getBoundingClientRect();
// Shorts обычно занимают большую часть экрана
if (rect.width > 100 && rect.height > 200) {
return video;
}
} else {
return video;
}
}
}
// Fallback - первое видео
return document.querySelector('video');
}
async function applyQualitySettings(video, quality) {
if (recordingSettings.audioOnly || quality === 'original' || quality === 'audio') return;
const desiredHeight = parseInt(quality);
if (!video || !video.videoHeight) return;
if (!video.dataset.originalWidth) {
video.dataset.originalWidth = video.style.width;
video.dataset.originalHeight = video.style.height;
}
const scale = desiredHeight / video.videoHeight;
if (scale < 1) {
video.style.width = (video.videoWidth * scale) + 'px';
video.style.height = desiredHeight + 'px';
}
}
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "ping") {
sendResponse({ status: "ok" });
return true;
}
if (request.action === "toggle_recording") {
if (recorder && recorder.state !== "inactive") {
stopRecording(sendResponse);
} else {
sendResponse({ status: "not_recording" });
}
return true;
}
if (request.action === "start_recording") {
startRecording(request.settings, sendResponse);
return true;
}
if (request.action === "stop_recording") {
stopRecording(sendResponse);
return true;
}
return true;
});
async function startRecording(settings, sendResponse) {
const video = getVideoElement();
if (!video) {
alert("Видео не найдено!");
sendResponse({ status: "error", message: "Видео не найдено! Воспроизведите видео на YouTube." });
return;
}
recordingSettings = settings || {};
originalVideoTitle = getVideoTitle();
// ИСПРАВЛЕНИЕ #1: Тайминги теперь работают и для аудио!
if (recordingSettings.trim && recordingSettings.trim.start > 0) {
video.currentTime = recordingSettings.trim.start;
// Даем время на перемотку
await new Promise(r => setTimeout(r, 300));
}
try {
// Захватываем поток (видео + аудио)
const stream = video.captureStream ? video.captureStream() : video.mozCaptureStream();
// Настройка аудио-мостика, чтобы звук не пропадал в колонках
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
streamSource = audioContext.createMediaStreamSource(stream);
streamSource.connect(audioContext.destination);
} else if (audioContext.state === 'suspended') {
audioContext.resume();
// ========== РЕЖИМ ТОЛЬКО АУДИО ==========
if (recordingSettings.audioOnly) {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
const source = audioContext.createMediaElementSource(video);
audioDestination = audioContext.createMediaStreamDestination();
// ВАЖНО: подключаем К ОБОИМ выходам! Иначе звук пропадёт!
source.connect(audioDestination); // Для записи
source.connect(audioContext.destination); // Для колонок!
mediaStream = audioDestination.stream;
}
// ========== РЕЖИМ ВИДЕО + АУДИО ==========
else {
if (video.mozCaptureStream) {
mediaStream = video.mozCaptureStream();
} else if (video.captureStream) {
mediaStream = video.captureStream();
} else {
throw new Error("Браузер не поддерживает захват видео");
}
await applyQualitySettings(video, recordingSettings.quality);
const audioTracks = mediaStream.getAudioTracks();
if (audioTracks.length > 0) {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
const streamSource = audioContext.createMediaStreamSource(mediaStream);
streamSource.connect(audioContext.destination);
} else {
try {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
const source = audioContext.createMediaElementSource(video);
audioDestination = audioContext.createMediaStreamDestination();
source.connect(audioDestination);
source.connect(audioContext.destination);
audioDestination.stream.getAudioTracks().forEach(track => {
mediaStream.addTrack(track);
});
} catch(e) {
console.log('Audio setup warning:', e);
}
}
}
// Настройка записи
recorder = new MediaRecorder(stream, {
mimeType: 'video/webm; codecs=vp8,opus'
});
const mimeType = recordingSettings.audioOnly ? 'audio/webm' : 'video/webm; codecs=vp9,opus';
const options = { mimeType };
if (!MediaRecorder.isTypeSupported(mimeType)) {
options.mimeType = recordingSettings.audioOnly ? 'audio/webm' : 'video/webm';
}
recorder = new MediaRecorder(mediaStream, options);
recordedChunks = [];
recorder.ondataavailable = (e) => {
if (e.data.size > 0) recordedChunks.push(e.data);
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
};
recorder.onstop = () => {
recorder.onstop = async () => {
await finalizeRecording(video);
};
recorder.onerror = (e) => {
console.error('Recorder error:', e);
toggleIndicator(false);
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `yt-record-${Date.now()}.webm`;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
};
recorder.start();
if (recordingSettings.timer > 0) {
const durationMs = recordingSettings.timer * 60 * 1000;
stopTimeout = setTimeout(() => {
if (recorder && recorder.state !== "inactive") {
recorder.stop();
}
}, durationMs);
}
// ИСПРАВЛЕНИЕ #1: Тайминги теперь работают и для аудио!
if (recordingSettings.trim && recordingSettings.trim.end > 0) {
checkTimeInterval = setInterval(() => {
if (video.currentTime >= recordingSettings.trim.end) {
clearInterval(checkTimeInterval);
checkTimeInterval = null;
if (recorder && recorder.state !== "inactive") {
recorder.stop();
}
}
}, 500);
}
recorder.start(1000);
toggleIndicator(true);

@@ -102,13 +353,133 @@ sendResponse({ status: "recording" });

console.error("Ошибка захвата:", err);
sendResponse({ status: "error", message: err.message });
}
}
if (request.action === "stop_recording") {
function stopRecording(sendResponse) {
if (stopTimeout) {
clearTimeout(stopTimeout);
stopTimeout = null;
}
if (checkTimeInterval) {
clearInterval(checkTimeInterval);
checkTimeInterval = null;
}
if (recorder && recorder.state !== "inactive") {
recorder.stop();
sendResponse({ status: "stopped" });
if (sendResponse) sendResponse({ status: "stopped" });
} else {
if (sendResponse) sendResponse({ status: "not_recording" });
}
}
// Возвращаем true для асинхронного ответа
return true;
});
async function finalizeRecording(video) {
toggleIndicator(false);
const isAudioOnly = recordingSettings.audioOnly;
let blob = new Blob(recordedChunks, {
type: isAudioOnly ? 'audio/webm' : 'video/webm'
});
const extension = isAudioOnly ? 'webm' : 'webm';
const filename = `${originalVideoTitle}.${extension}`;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
if (recordingSettings.cloud) {
await uploadToCloud(blob, filename, recordingSettings.cloud);
}
setTimeout(() => {
if (a.parentNode) document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
if (!isAudioOnly && video && video.dataset.originalWidth) {
video.style.width = video.dataset.originalWidth;
video.style.height = video.dataset.originalHeight;
delete video.dataset.originalWidth;
delete video.dataset.originalHeight;
}
if (mediaStream) {
if (isAudioOnly) {
mediaStream.getTracks().forEach(track => track.stop());
} else {
mediaStream.getVideoTracks().forEach(track => track.stop());
}
}
recordedChunks = [];
recorder = null;
mediaStream = null;
audioDestination = null;
}
async function uploadToCloud(blob, filename, settings) {
if (!settings || !settings.token) return;
try {
switch(settings.provider) {
case 'dropbox':
await uploadToDropbox(blob, filename, settings.token);
break;
case 'gdrive':
await uploadToGoogleDrive(blob, filename, settings.token);
break;
case 'cloudinary':
await uploadToCloudinary(blob, filename, settings.token);
break;
}
} catch (err) {
console.error('Ошибка загрузки в облако:', err);
}
}
async function uploadToDropbox(blob, filename, token) {
const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Dropbox-API-Arg': JSON.stringify({
path: '/YouTube Recordings/' + filename,
mode: 'add',
autorename: true
}),
'Content-Type': 'application/octet-stream'
},
body: blob
});
return response.json();
}
async function uploadToCloudinary(blob, filename, token) {
const formData = new FormData();
formData.append('file', blob, filename);
formData.append('upload_preset', token);
const response = await fetch('https://api.cloudinary.com/v1_1/demo/video/upload', {
method: 'POST',
body: formData
});
return response.json();
}
async function uploadToGoogleDrive(blob, filename, token) {
const metadata = {
name: filename,
mimeType: blob.type
};
const formData = new FormData();
formData.append('metadata', new Blob([JSON.stringify(metadata)], {type: 'application/json'}));
formData.append('file', blob);
const response = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
});
return response.json();
}
}
+21
-13
{
"manifest_version": 2,
"name": "YouTube Recorder",
"version": "2.8",
"description": "\u0417\u0430\u043f\u0438\u0441\u044c \u0432\u0438\u0434\u0435\u043e \u0441 YouTube \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0438\u0437 \u043f\u043b\u0435\u0435\u0440\u0430 \u0441\u043e \u0437\u0432\u0443\u043a\u043e\u043c.",
"name": "YouTube Recorder Pro",
"version": "3.4",
"description": "\u0417\u0430\u043f\u0438\u0441\u044c \u0432\u0438\u0434\u0435\u043e \u0441 YouTube \u0438 Shorts \u0441 \u0432\u044b\u0431\u043e\u0440\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430, \u0442\u0430\u0439\u043c\u0435\u0440\u043e\u043c, \u0442\u0440\u0438\u043c\u043c\u0438\u043d\u0433\u043e\u043c \u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438",
"author": "Your Name",

@@ -10,8 +10,3 @@ "browser_specific_settings": {

"id": "yt-recorder-unique-id@example.com",
"strict_min_version": "109.0",
"data_collection_permissions": {
"required": [
"none"
]
}
"strict_min_version": "109.0"
}

@@ -23,3 +18,4 @@ },

"downloads",
"https://www.youtube.com/*"
"storage",
"<all_urls>"
],

@@ -42,3 +38,3 @@ "background": {

"matches": [
"https://www.youtube.com/*"
"*://*.youtube.com/*"
],

@@ -48,5 +44,14 @@ "js": [

],
"run_at": "document_idle"
"run_at": "document_idle",
"all_frames": false
}
],
"commands": {
"toggle-recording": {
"suggested_key": {
"default": "Ctrl+Shift+Y"
},
"description": "\u041d\u0430\u0447\u0430\u0442\u044c/\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c"
}
},
"icons": {

@@ -56,3 +61,6 @@ "16": "icons/16.png",

"96": "icons/96.png"
}
},
"web_accessible_resources": [
"icons/*.png"
]
}

@@ -6,14 +6,249 @@ <!DOCTYPE html>

<style>
body { width: 200px; padding: 15px; text-align: center; font-family: sans-serif; }
button { width: 100%; padding: 10px; margin: 5px 0; cursor: pointer; border: none; border-radius: 4px; color: white; }
#start { background: #cc0000; }
#stop { background: #333; display: none; }
body {
width: 360px;
padding: 15px;
font-family: 'Segoe UI', sans-serif;
background: #f5f5f5;
max-height: 600px;
overflow-y: auto;
}
h3 {
margin: 0 0 15px 0;
color: #cc0000;
text-align: center;
font-size: 18px;
}
.section {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.section-title {
font-weight: bold;
margin-bottom: 8px;
color: #333;
font-size: 12px;
text-transform: uppercase;
}
select, input[type="number"], input[type="text"] {
width: 100%;
padding: 8px;
margin: 4px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.row {
display: flex;
gap: 8px;
}
.row input {
flex: 1;
}
button {
width: 100%;
padding: 12px;
margin: 8px 0;
cursor: pointer;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
font-size: 14px;
transition: all 0.2s;
}
button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
#start {
background: linear-gradient(135deg, #cc0000, #990000);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#stop {
background: linear-gradient(135deg, #333, #000);
display: none;
}
#trimControls {
display: none;
}
.checkbox-container {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.checkbox-container input[type="checkbox"] {
width: auto;
}
.timer-display {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #cc0000;
margin: 10px 0;
font-family: monospace;
}
.cloud-options {
display: none;
margin-top: 8px;
}
.status {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 8px;
min-height: 16px;
}
/* Стили для инструкции */
.help-section {
background: #e3f2fd;
border-left: 4px solid #2196f3;
}
.help-title {
color: #1976d2;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.help-title:hover {
color: #0d47a1;
}
.help-content {
display: none;
margin-top: 10px;
font-size: 12px;
line-height: 1.5;
color: #333;
}
.help-content.active {
display: block;
}
.help-step {
margin-bottom: 8px;
padding-left: 20px;
position: relative;
}
.help-step::before {
content: "➤";
position: absolute;
left: 0;
color: #2196f3;
}
.help-warning {
background: #fff3e0;
border-left: 3px solid #ff9800;
padding: 8px;
margin-top: 10px;
border-radius: 4px;
}
.help-tip {
background: #e8f5e9;
border-left: 3px solid #4caf50;
padding: 8px;
margin-top: 8px;
border-radius: 4px;
}
.toggle-icon {
transition: transform 0.3s;
}
.toggle-icon.rotated {
transform: rotate(180deg);
}
</style>
</head>
<body>
<h3>YouTube Recorder</h3>
<button id="start">Начать запись</button>
<button id="stop">Остановить</button>
<h3>🎬 YouTube Recorder Pro</h3>
<!-- ИНСТРУКЦИЯ -->
<div class="section help-section">
<div class="section-title help-title" id="helpHeader">
<span class="toggle-icon" id="helpIcon">▼</span>
📖 КАК ПОЛЬЗОВАТЬСЯ (нажми чтобы открыть)
</div>
<div class="help-content" id="helpContent">
<div class="help-step"><b>Шаг 1:</b> Откройте любое видео на YouTube и начните его воспроизведение</div>
<div class="help-step"><b>Шаг 2:</b> Выберите качество записи (оригинал, 1080p, 720p, 480p или только аудио)</div>
<div class="help-step"><b>Шаг 3:</b> Нажмите кнопку "Начать запись" — появится красная точка REC</div>
<div class="help-step"><b>Шаг 4:</b> Для остановки нажмите кнопку "⏹ Остановить запись" на странице или в этом окне</div>
<div class="help-step"><b>Шаг 5:</b> Файл автоматически скачается на ваш компьютер</div>
<div class="help-warning">
<b>⚠️ Важно:</b> Не закрывайте вкладку YouTube во время записи! Запись идёт только пока открыта страница.
</div>
<div class="help-tip">
<b>💡 Совет:</b> Для записи фрагмента включите "Записать определённый отрезок" и укажите время начала и конца в секундах или минутах:секундах (например: 01:30)
</div>
<div class="help-tip">
<b>💡 Горячая клавиша:</b> Ctrl+Shift+Y — быстрый стоп/старт записи (когда открыто YouTube)
</div>
</div>
</div>
<div class="section">
<div class="section-title">Качество записи</div>
<select id="quality">
<option value="original">🎯 Оригинальное качество</option>
<option value="1080">1080p (Full HD)</option>
<option value="720">720p (HD)</option>
<option value="480">480p</option>
<option value="audio">🎵 Только аудио (MP3)</option>
</select>
</div>
<div class="section" id="timeSection">
<div class="section-title">Временной интервал</div>
<div class="checkbox-container">
<input type="checkbox" id="enableTrim">
<label for="enableTrim">Записать определённый отрезок</label>
</div>
<div id="trimControls">
<div class="row">
<input type="text" id="startTime" placeholder="Начало (сек или ММ:СС)">
<input type="text" id="endTime" placeholder="Конец (сек или ММ:СС)">
</div>
</div>
</div>
<div class="section">
<div class="section-title">Таймер автоматической остановки</div>
<div class="checkbox-container">
<input type="checkbox" id="enableTimer">
<label for="enableTimer">Включить таймер</label>
</div>
<input type="number" id="timerMinutes" placeholder="Минут" min="1" max="180" style="display:none;">
<div id="timerDisplay" class="timer-display" style="display:none;">00:00</div>
</div>
<div class="section">
<div class="section-title">Облачное хранилище (опционально)</div>
<div class="checkbox-container">
<input type="checkbox" id="enableCloud">
<label for="enableCloud">Автозагрузка в облако</label>
</div>
<select id="cloudProvider" class="cloud-options">
<option value="dropbox">Dropbox</option>
<option value="gdrive">Google Drive</option>
<option value="cloudinary">Cloudinary</option>
</select>
<input type="text" id="cloudToken" class="cloud-options" placeholder="API ключ / токен доступа">
</div>
<button id="start">
<span>⏺</span> Начать запись
</button>
<button id="stop">⏹ Остановить запись</button>
<div id="status" class="status"></div>
<script src="popup.js"></script>
</body>
</html>
+185
-28

@@ -1,40 +0,197 @@

const api = typeof browser !== "undefined" ? browser : chrome;
let isRecording = false;
let timerInterval;
let secondsElapsed = 0;
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
// Функция для показа/скрытия инструкции - ДОЛЖНА БЫТЬ ГЛОБАЛЬНОЙ
window.toggleHelp = function() {
const content = document.getElementById('helpContent');
const icon = document.getElementById('helpIcon');
if (content && icon) {
content.classList.toggle('active');
icon.classList.toggle('rotated');
}
};
startBtn.onclick = async () => {
try {
const tabs = await api.tabs.query({ active: true, currentWindow: true });
if (!tabs || tabs.length === 0) return;
document.addEventListener('DOMContentLoaded', () => {
const helpHeader = document.getElementById('helpHeader');
if (helpHeader) {
helpHeader.addEventListener('click', toggleHelp);
}
const activeTab = tabs[0];
if (!activeTab.url.includes("youtube.com")) {
alert("Зайдите на YouTube!");
return;
const startBtn = document.getElementById('start');
const stopBtn = document.getElementById('stop');
const enableTrim = document.getElementById('enableTrim');
const trimControls = document.getElementById('trimControls');
const enableTimer = document.getElementById('enableTimer');
const timerInput = document.getElementById('timerMinutes');
const timerDisplay = document.getElementById('timerDisplay');
const enableCloud = document.getElementById('enableCloud');
const cloudOptions = document.querySelectorAll('.cloud-options');
const statusDiv = document.getElementById('status');
// Переключение контролов обрезки
enableTrim.addEventListener('change', (e) => {
trimControls.style.display = e.target.checked ? 'block' : 'none';
});
// Переключение таймера
enableTimer.addEventListener('change', (e) => {
timerInput.style.display = e.target.checked ? 'block' : 'none';
if (!e.target.checked) {
timerDisplay.style.display = 'none';
clearInterval(timerInterval);
}
});
api.tabs.sendMessage(activeTab.id, { action: "start_recording" }, (response) => {
if (api.runtime.lastError) {
alert("Обновите страницу YouTube (F5).");
} else if (response && response.status === "recording") {
// Переключение облака
enableCloud.addEventListener('change', (e) => {
cloudOptions.forEach(el => el.style.display = e.target.checked ? 'block' : 'none');
});
// Проверка, является ли URL YouTube Shorts
function isYouTubeShorts(url) {
return url && url.includes('youtube.com/shorts');
}
// Проверка, является ли URL обычным YouTube видео
function isYouTubeVideo(url) {
return url && (url.includes('youtube.com/watch') || url.includes('youtu.be/'));
}
// Старт записи
startBtn.addEventListener('click', async () => {
try {
const tabs = await browser.tabs.query({active: true, currentWindow: true});
const tab = tabs[0];
// Проверяем URL - поддерживаем обычные видео и шорты
if (!tab.url || (!isYouTubeVideo(tab.url) && !isYouTubeShorts(tab.url))) {
statusDiv.textContent = '❌ Откройте страницу YouTube (видео или Shorts)';
return;
}
const settings = {
quality: document.getElementById('quality').value,
audioOnly: document.getElementById('quality').value === 'audio',
trim: enableTrim.checked ? {
start: parseTime(document.getElementById('startTime').value),
end: parseTime(document.getElementById('endTime').value)
} : null,
timer: enableTimer.checked ? parseInt(document.getElementById('timerMinutes').value) || 0 : 0,
cloud: enableCloud.checked ? {
provider: document.getElementById('cloudProvider').value,
token: document.getElementById('cloudToken').value
} : null,
isShorts: isYouTubeShorts(tab.url) // Флаг для content.js
};
if (settings.trim && settings.trim.start >= settings.trim.end && settings.trim.end > 0) {
statusDiv.textContent = '❌ Время начала должно быть меньше времени окончания';
return;
}
// Проверяем, загружен ли content script
let scriptLoaded = false;
try {
const pingResponse = await browser.tabs.sendMessage(tab.id, {action: "ping"});
if (pingResponse && pingResponse.status === "ok") {
scriptLoaded = true;
}
} catch (e) {
scriptLoaded = false;
}
// Если не загружен, пробуем вставить
if (!scriptLoaded) {
try {
await browser.tabs.executeScript(tab.id, {
file: "content.js"
});
await new Promise(r => setTimeout(r, 200));
} catch (injectErr) {
console.log('Script injection failed:', injectErr);
}
}
// Отправляем команду начала записи
const response = await browser.tabs.sendMessage(tab.id, {
action: "start_recording",
settings: settings
});
if (response && response.status === "recording") {
isRecording = true;
startBtn.style.display = 'none';
stopBtn.style.display = 'block';
statusDiv.textContent = '🔴 Идёт запись...';
if (settings.timer > 0) {
startTimer(settings.timer);
}
} else if (response && response.status === "error") {
statusDiv.textContent = '❌ ' + (response.message || 'Ошибка');
}
});
} catch (err) {
console.error(err);
} catch (err) {
statusDiv.textContent = '❌ Ошибка: ' + err.message;
console.error(err);
}
});
// Стоп записи
stopBtn.addEventListener('click', async () => {
try {
const tabs = await browser.tabs.query({active: true, currentWindow: true});
await browser.tabs.sendMessage(tabs[0].id, {
action: "stop_recording"
});
stopRecording();
} catch (err) {
console.error(err);
stopRecording();
}
});
function stopRecording() {
isRecording = false;
startBtn.style.display = 'block';
stopBtn.style.display = 'none';
clearInterval(timerInterval);
timerDisplay.style.display = 'none';
statusDiv.textContent = '✅ Запись сохранена';
setTimeout(() => statusDiv.textContent = '', 3000);
}
};
stopBtn.onclick = async () => {
const tabs = await api.tabs.query({ active: true, currentWindow: true });
if (tabs && tabs[0]) {
api.tabs.sendMessage(tabs[0].id, { action: "stop_recording" }, (response) => {
if (response && response.status === "stopped") {
startBtn.style.display = 'block';
stopBtn.style.display = 'none';
function startTimer(minutes) {
let remaining = minutes * 60;
timerDisplay.style.display = 'block';
timerInterval = setInterval(() => {
remaining--;
secondsElapsed++;
const mins = Math.floor(remaining / 60).toString().padStart(2, '0');
const secs = (remaining % 60).toString().padStart(2, '0');
timerDisplay.textContent = `${mins}:${secs}`;
if (remaining <= 0) {
clearInterval(timerInterval);
document.getElementById('stop').click();
}
});
}, 1000);
}
};
function parseTime(timeStr) {
if (!timeStr) return 0;
if (timeStr.includes(':')) {
const parts = timeStr.split(':').map(Number);
if (parts.length === 2) {
return parts[0] * 60 + parts[1];
} else if (parts.length === 3) {
return parts[0] * 3600 + parts[1] * 60 + parts[2];
}
}
return parseInt(timeStr) || 0;
}
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet