Implement download functionality and volume control for Instagram Reels

This commit is contained in:
Илья Глазунов 2026-01-20 21:56:31 +03:00
parent 0fa9c8599f
commit 4a3c25c369
4 changed files with 439 additions and 291 deletions

View File

@ -1,7 +1,173 @@
// Background Service Worker for Reels Master // Background Service Worker for Reels Master
console.log('Reels Master: Background service worker loaded'); console.log('Reels Master: Background service worker loaded');
// Listen for extension installation const ENCODING_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
function shortcodeToPk(shortcode: string): bigint {
if (shortcode.length > 28) {
shortcode = shortcode.slice(0, -28);
}
let pk = BigInt(0);
for (const char of shortcode) {
pk = pk * BigInt(64) + BigInt(ENCODING_CHARS.indexOf(char));
}
return pk;
}
function extractShortcode(url: string): string | null {
const match = url.match(/\/(?:p|tv|reels?(?!\/audio\/))\/([^/?#&]+)/);
return match ? match[1] : null;
}
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
console.log('Reels Master: Extension installed'); console.log('Reels Master: Extension installed');
}); });
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'DOWNLOAD_REEL') {
handleDownload(message.url)
.then(result => sendResponse(result))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
}
});
async function handleDownload(reelUrl: string): Promise<{ success: boolean; downloadUrl?: string; error?: string }> {
try {
console.log('Reels Master: Processing download for', reelUrl);
const shortcode = extractShortcode(reelUrl);
if (!shortcode) {
return { success: false, error: 'Could not extract shortcode from URL' };
}
const pk = shortcodeToPk(shortcode);
console.log('Reels Master: Shortcode:', shortcode, 'PK:', pk.toString());
const apiHeaders = {
'X-IG-App-ID': '936619743392459',
'X-ASBD-ID': '198387',
'X-IG-WWW-Claim': '0',
'Origin': 'https://www.instagram.com',
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
};
const apiUrl = `https://i.instagram.com/api/v1/media/${pk}/info/`;
console.log('Reels Master: Fetching from API:', apiUrl);
const response = await fetch(apiUrl, {
method: 'GET',
headers: apiHeaders,
credentials: 'include',
});
if (!response.ok) {
console.log('Reels Master: API response not ok:', response.status);
return await tryGraphQLFallback(shortcode, apiHeaders);
}
const data = await response.json();
console.log('Reels Master: API response received');
const videoUrl = extractVideoUrl(data);
if (!videoUrl) {
console.log('Reels Master: No video URL in API response, trying fallback');
return await tryGraphQLFallback(shortcode, apiHeaders);
}
console.log('Reels Master: Found video URL, starting download');
await chrome.downloads.download({
url: videoUrl,
filename: `reel_${shortcode}_${Date.now()}.mp4`,
});
return { success: true, downloadUrl: videoUrl };
} catch (error) {
console.error('Reels Master: Download error', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async function tryGraphQLFallback(shortcode: string, headers: Record<string, string>): Promise<{ success: boolean; downloadUrl?: string; error?: string }> {
try {
console.log('Reels Master: Trying GraphQL fallback');
const variables = {
shortcode: shortcode,
child_comment_count: 3,
fetch_comment_count: 40,
parent_comment_count: 24,
has_threaded_comments: true,
};
const graphqlUrl = `https://www.instagram.com/graphql/query/?doc_id=8845758582119845&variables=${encodeURIComponent(JSON.stringify(variables))}`;
const response = await fetch(graphqlUrl, {
method: 'GET',
headers: {
...headers,
'X-Requested-With': 'XMLHttpRequest',
'Referer': `https://www.instagram.com/reel/${shortcode}/`,
},
credentials: 'include',
});
if (!response.ok) {
return { success: false, error: `GraphQL request failed: ${response.status}` };
}
const data = await response.json();
const media = data?.data?.xdt_shortcode_media;
if (!media) {
return { success: false, error: 'No media data in GraphQL response. Try logging in to Instagram.' };
}
const videoUrl = media.video_url;
if (!videoUrl) {
return { success: false, error: 'No video URL found in response' };
}
await chrome.downloads.download({
url: videoUrl,
filename: `reel_${shortcode}_${Date.now()}.mp4`,
});
return { success: true, downloadUrl: videoUrl };
} catch (error) {
console.error('Reels Master: GraphQL fallback error', error);
return { success: false, error: error instanceof Error ? error.message : 'GraphQL fallback failed' };
}
}
function extractVideoUrl(data: any): string | null {
const items = data?.items;
if (!items || !items.length) return null;
const item = items[0];
if (item.video_url) {
return item.video_url;
}
const videoVersions = item.video_versions;
if (videoVersions && videoVersions.length > 0) {
return videoVersions[0].url;
}
const carouselMedia = item.carousel_media;
if (carouselMedia && carouselMedia.length > 0) {
for (const media of carouselMedia) {
if (media.video_versions && media.video_versions.length > 0) {
return media.video_versions[0].url;
}
}
}
return null;
}

View File

@ -193,3 +193,17 @@
.reels-master-download:hover::after { .reels-master-download:hover::after {
opacity: 1; opacity: 1;
} }
/* Spinner animation for download */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.reels-master-spinner {
animation: spin 1s linear infinite;
}

View File

@ -2,31 +2,21 @@ import './content.css';
console.log('Reels Master: Content script loaded'); console.log('Reels Master: Content script loaded');
interface ReelsControls {
volumeSlider: HTMLInputElement | null;
downloadButton: HTMLButtonElement | null;
container: HTMLDivElement | null;
}
class ReelsMaster { class ReelsMaster {
private currentVideo: HTMLVideoElement | null = null;
private controls: ReelsControls = {
volumeSlider: null,
downloadButton: null,
container: null
};
private observer: MutationObserver | null = null;
private storedVolume: number = 0.5; private storedVolume: number = 0.5;
private storedMuted: boolean = false; private storedMuted: boolean = false;
private videoVolumeListeners: WeakMap<HTMLVideoElement, () => void> = new WeakMap(); private processedContainers: WeakSet<HTMLElement> = new WeakSet();
private videoVolumeListeners: WeakMap<HTMLVideoElement, boolean> = new WeakMap();
private domObserver: MutationObserver | null = null;
constructor() { constructor() {
this.init(); this.init();
} }
private init(): void { private init(): void {
// Сразу начинаем следить за всеми видео для мгновенного применения громкости this.loadSettings();
this.setupGlobalVideoInterceptor();
this.setupVideoInterceptor();
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.start()); document.addEventListener('DOMContentLoaded', () => this.start());
@ -35,25 +25,47 @@ class ReelsMaster {
} }
} }
// Перехватываем все видео сразу при их появлении и применяем сохраненную громкость private loadSettings(): void {
private setupGlobalVideoInterceptor(): void { if (typeof chrome !== 'undefined' && chrome.storage?.local) {
// Применяем к уже существующим видео chrome.storage.local.get(['volume', 'muted'], (result) => {
if (result.volume !== undefined) {
this.storedVolume = result.volume;
}
if (result.muted !== undefined) {
this.storedMuted = result.muted;
}
this.applyVolumeToAllVideos();
this.updateAllSliders();
});
}
}
private saveSettings(): void {
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
chrome.storage.local.set({
volume: this.storedVolume,
muted: this.storedMuted
});
}
}
private start(): void {
console.log('Reels Master: Starting...');
this.injectControlsToAllContainers();
this.setupDOMObserver();
}
private setupVideoInterceptor(): void {
this.applyVolumeToAllVideos(); this.applyVolumeToAllVideos();
// Следим за новыми видео через MutationObserver
const videoObserver = new MutationObserver((mutations) => { const videoObserver = new MutationObserver((mutations) => {
let hasNewVideo = false;
for (const mutation of mutations) { for (const mutation of mutations) {
for (const node of mutation.addedNodes) { for (const node of mutation.addedNodes) {
if (node instanceof HTMLVideoElement) { if (node instanceof HTMLVideoElement) {
hasNewVideo = true;
this.applyVolumeToVideo(node); this.applyVolumeToVideo(node);
} else if (node instanceof HTMLElement) { } else if (node instanceof HTMLElement) {
const videos = node.querySelectorAll('video'); const videos = node.querySelectorAll('video');
if (videos.length > 0) { videos.forEach(video => this.applyVolumeToVideo(video));
hasNewVideo = true;
videos.forEach(video => this.applyVolumeToVideo(video));
}
} }
} }
} }
@ -66,6 +78,8 @@ class ReelsMaster {
} }
private applyVolumeToAllVideos(): void { private applyVolumeToAllVideos(): void {
if (!window.location.pathname.includes('/reels/')) return;
document.querySelectorAll('video').forEach(video => { document.querySelectorAll('video').forEach(video => {
this.applyVolumeToVideo(video); this.applyVolumeToVideo(video);
}); });
@ -74,202 +88,135 @@ class ReelsMaster {
private applyVolumeToVideo(video: HTMLVideoElement): void { private applyVolumeToVideo(video: HTMLVideoElement): void {
if (!window.location.pathname.includes('/reels/')) return; if (!window.location.pathname.includes('/reels/')) return;
// Применяем сохраненную громкость
video.volume = this.storedVolume; video.volume = this.storedVolume;
video.muted = this.storedMuted; video.muted = this.storedMuted;
// Добавляем слушатель на случай если Instagram перезапишет громкость
if (!this.videoVolumeListeners.has(video)) { if (!this.videoVolumeListeners.has(video)) {
const listener = () => { this.videoVolumeListeners.set(video, true);
// Если громкость изменилась не нами, восстанавливаем
if (Math.abs(video.volume - this.storedVolume) > 0.01 || video.muted !== this.storedMuted) { let changeCount = 0;
const maxChanges = 10;
const enforceVolume = () => {
changeCount++;
if (changeCount <= maxChanges) {
video.volume = this.storedVolume; video.volume = this.storedVolume;
video.muted = this.storedMuted; video.muted = this.storedMuted;
} }
}; };
// Слушаем первые несколько изменений громкости для борьбы с Instagram video.addEventListener('volumechange', enforceVolume);
let volumeChangeCount = 0; video.addEventListener('loadedmetadata', enforceVolume);
const tempListener = () => { video.addEventListener('play', enforceVolume);
volumeChangeCount++; video.addEventListener('canplay', enforceVolume);
if (volumeChangeCount <= 5) {
video.volume = this.storedVolume;
video.muted = this.storedMuted;
} else {
video.removeEventListener('volumechange', tempListener);
}
};
video.addEventListener('volumechange', tempListener);
video.addEventListener('loadedmetadata', listener);
video.addEventListener('play', listener);
this.videoVolumeListeners.set(video, listener);
} }
} }
private start(): void { private setupDOMObserver(): void {
console.log('Reels Master: Starting...'); this.domObserver = new MutationObserver((mutations) => {
this.checkForReels(); if (!window.location.pathname.includes('/reels/')) return;
this.observeUrlChanges();
this.observeDOMChanges(); let shouldCheck = false;
window.addEventListener('scroll', () => {
this.checkForReels(); for (const mutation of mutations) {
}, { passive: true }); for (const node of mutation.addedNodes) {
} if (node instanceof HTMLElement) {
if (node.querySelector('video') || node.querySelector('svg[aria-label="Like"]')) {
private observeUrlChanges(): void { shouldCheck = true;
let lastUrl = location.href; break;
new MutationObserver(() => { }
const currentUrl = location.href; }
if (currentUrl !== lastUrl) { }
lastUrl = currentUrl; if (shouldCheck) break;
console.log('Reels Master: URL changed to', currentUrl); }
// Сразу применяем громкость ко всем видео
this.applyVolumeToAllVideos(); if (shouldCheck) {
setTimeout(() => this.checkForReels(), 300); requestAnimationFrame(() => {
this.injectControlsToAllContainers();
});
} }
}).observe(document.querySelector('body')!, {
subtree: true,
childList: true
});
}
private observeDOMChanges(): void {
this.observer = new MutationObserver(() => {
this.checkForReels();
}); });
this.observer.observe(document.body, { this.domObserver.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true
}); });
} }
private checkForReels(): void { private injectControlsToAllContainers(): void {
if (!window.location.pathname.includes('/reels/')) { if (!window.location.pathname.includes('/reels/')) return;
this.cleanup();
return;
}
const video = this.getActiveVideo(); const actionContainers = this.findAllActionContainers();
if (!video || video === this.currentVideo) {
return; console.log(`Reels Master: Found ${actionContainers.length} action containers`);
}
console.log('Reels Master: Found new video element'); for (const container of actionContainers) {
this.currentVideo = video; this.injectControlsToContainer(container);
}
// Убедимся что громкость применена к текущему видео
this.applyVolumeToVideo(video);
this.injectControls();
} }
private getActiveVideo(): HTMLVideoElement | null { private findAllActionContainers(): HTMLElement[] {
const videos = Array.from(document.querySelectorAll('video')); const containers: HTMLElement[] = [];
if (videos.length === 0) return null;
const likeButtons = document.querySelectorAll('svg[aria-label="Like"]');
const center = window.innerHeight / 2;
let closestVideo: HTMLVideoElement | null = null; for (const likeButton of likeButtons) {
let minDistance = Infinity; const container = this.findActionContainerFromLikeButton(likeButton);
if (container && !containers.includes(container)) {
for (const video of videos) { containers.push(container);
const rect = video.getBoundingClientRect();
if (rect.height === 0) continue;
const videoCenter = rect.top + (rect.height / 2);
const distance = Math.abs(center - videoCenter);
if (distance < minDistance) {
minDistance = distance;
closestVideo = video;
} }
} }
return closestVideo;
}
private injectControls(): void {
if (!this.currentVideo) return;
// Ищем контейнер кнопок рядом с ТЕКУЩИМ видео, а не глобально
const actionButtons = this.findActionButtonsContainer(this.currentVideo);
if (!actionButtons) {
console.log('Reels Master: Action buttons container not found');
setTimeout(() => this.injectControls(), 500);
return;
}
// Проверяем, есть ли уже наши контролы в этом контейнере
const existingControls = actionButtons.querySelector('.reels-master-controls');
if (existingControls) {
console.log('Reels Master: Controls already exist in this container');
return;
}
// Удаляем старые контролы из предыдущего контейнера
if (this.controls.container && this.controls.container.parentElement !== actionButtons) {
this.controls.container.remove();
}
this.controls.container = this.createControlsContainer();
this.controls.volumeSlider = this.createVolumeSlider();
this.controls.downloadButton = this.createDownloadButton();
this.controls.container.appendChild(this.createVolumeControl());
this.controls.container.appendChild(this.controls.downloadButton);
actionButtons.insertBefore(this.controls.container, actionButtons.firstChild);
// Синхронизируем слайдер с текущей громкостью
if (this.controls.volumeSlider) {
this.controls.volumeSlider.value = String(this.storedVolume * 100);
}
console.log('Reels Master: Controls injected');
}
// Ищем контейнер кнопок относительно конкретного видео
private findActionButtonsContainer(video: HTMLVideoElement): HTMLElement | null {
// Ищем родительский контейнер рила для данного видео
let reelContainer = video.closest('article') || video.closest('[role="presentation"]');
if (!reelContainer) { return containers;
// Пробуем найти родителя вверх по дереву }
let parent = video.parentElement;
for (let i = 0; i < 10 && parent; i++) { private findActionContainerFromLikeButton(likeButton: Element): HTMLElement | null {
if (parent.querySelector('svg[aria-label="Like"]')) { let parent = likeButton.parentElement;
reelContainer = parent;
break; while (parent) {
const hasLike = parent.querySelector('svg[aria-label="Like"]');
const hasComment = parent.querySelector('svg[aria-label="Comment"]');
const hasShare = parent.querySelector('svg[aria-label="Share"]');
const hasSave = parent.querySelector('svg[aria-label="Save"]');
if (hasLike && hasComment && hasShare && hasSave) {
const children = parent.children;
if (children.length >= 4) {
return parent as HTMLElement;
} }
parent = parent.parentElement;
} }
parent = parent.parentElement;
if (parent === document.body) break;
} }
if (!reelContainer) {
reelContainer = document.body;
}
// Ищем кнопку лайка внутри контейнера текущего рила
const likeButton = reelContainer.querySelector('svg[aria-label="Like"]');
if (likeButton) {
let parent = likeButton.parentElement;
while (parent && parent !== reelContainer) {
const childDivs = parent.querySelectorAll(':scope > div');
if (childDivs.length >= 3) {
const hasLike = parent.querySelector('svg[aria-label="Like"]');
const hasComment = parent.querySelector('svg[aria-label="Comment"]');
const hasShare = parent.querySelector('svg[aria-label="Share"]');
if (hasLike && hasComment && hasShare) {
return parent as HTMLElement;
}
}
parent = parent.parentElement;
}
}
return null; return null;
} }
private injectControlsToContainer(container: HTMLElement): void {
if (this.processedContainers.has(container)) {
return;
}
if (container.querySelector('.reels-master-controls')) {
this.processedContainers.add(container);
return;
}
const controlsContainer = this.createControlsContainer();
const volumeControl = this.createVolumeControl();
const downloadButton = this.createDownloadButton(container);
controlsContainer.appendChild(volumeControl);
controlsContainer.appendChild(downloadButton);
container.insertBefore(controlsContainer, container.firstChild);
this.processedContainers.add(container);
console.log('Reels Master: Controls injected to container');
}
private createControlsContainer(): HTMLDivElement { private createControlsContainer(): HTMLDivElement {
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'reels-master-controls'; container.className = 'reels-master-controls';
@ -280,32 +227,23 @@ class ReelsMaster {
const volumeControl = document.createElement('div'); const volumeControl = document.createElement('div');
volumeControl.className = 'reels-master-volume'; volumeControl.className = 'reels-master-volume';
const volumeButton = document.createElement('button');
volumeButton.className = 'reels-master-volume-button';
volumeButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
`;
const sliderContainer = document.createElement('div'); const sliderContainer = document.createElement('div');
sliderContainer.className = 'reels-master-slider-container'; sliderContainer.className = 'reels-master-slider-container';
this.controls.volumeSlider = this.createVolumeSlider(); const slider = this.createVolumeSlider();
sliderContainer.appendChild(this.controls.volumeSlider); sliderContainer.appendChild(slider);
volumeButton.onclick = () => {
if (this.currentVideo) {
this.storedMuted = !this.storedMuted;
// Применяем ко всем видео
this.applyVolumeToAllVideos();
if (this.controls.volumeSlider) { const volumeButton = document.createElement('button');
this.controls.volumeSlider.value = this.storedMuted ? '0' : String(this.storedVolume * 100); volumeButton.className = 'reels-master-volume-button';
} this.updateVolumeIcon(volumeButton);
this.updateVolumeIcon(volumeButton);
} volumeButton.onclick = () => {
this.storedMuted = !this.storedMuted;
this.saveSettings();
this.applyVolumeToAllVideos();
this.updateAllSliders();
this.updateAllVolumeIcons();
}; };
volumeControl.appendChild(sliderContainer); volumeControl.appendChild(sliderContainer);
@ -319,36 +257,39 @@ class ReelsMaster {
slider.type = 'range'; slider.type = 'range';
slider.min = '0'; slider.min = '0';
slider.max = '100'; slider.max = '100';
slider.value = this.storedMuted ? '0' : String(this.storedVolume * 100);
// Используем сохраненную громкость
slider.value = String(this.storedVolume * 100);
slider.className = 'reels-master-volume-slider'; slider.className = 'reels-master-volume-slider';
slider.oninput = (e) => { slider.oninput = (e) => {
if (this.currentVideo) { const value = parseInt((e.target as HTMLInputElement).value);
const value = parseInt((e.target as HTMLInputElement).value); this.storedVolume = value / 100;
const newVolume = value / 100; this.storedMuted = value === 0;
const newMuted = value === 0;
this.saveSettings();
// Сохраняем настройки this.applyVolumeToAllVideos();
this.storedVolume = newVolume; this.updateAllSliders();
this.storedMuted = newMuted; this.updateAllVolumeIcons();
// Применяем ко всем видео сразу
this.applyVolumeToAllVideos();
const volumeControl = slider.closest('.reels-master-volume');
const volumeButton = volumeControl?.querySelector('.reels-master-volume-button') as HTMLButtonElement;
if (volumeButton) {
this.updateVolumeIcon(volumeButton);
}
}
}; };
return slider; return slider;
} }
private updateAllSliders(): void {
const sliders = document.querySelectorAll('.reels-master-volume-slider') as NodeListOf<HTMLInputElement>;
const value = this.storedMuted ? '0' : String(this.storedVolume * 100);
sliders.forEach(slider => {
slider.value = value;
});
}
private updateAllVolumeIcons(): void {
const buttons = document.querySelectorAll('.reels-master-volume-button') as NodeListOf<HTMLButtonElement>;
buttons.forEach(button => {
this.updateVolumeIcon(button);
});
}
private updateVolumeIcon(button: HTMLButtonElement): void { private updateVolumeIcon(button: HTMLButtonElement): void {
const volume = this.storedMuted ? 0 : this.storedVolume; const volume = this.storedMuted ? 0 : this.storedVolume;
@ -370,7 +311,7 @@ class ReelsMaster {
button.innerHTML = icon; button.innerHTML = icon;
} }
private createDownloadButton(): HTMLButtonElement { private createDownloadButton(actionContainer: HTMLElement): HTMLButtonElement {
const button = document.createElement('button'); const button = document.createElement('button');
button.className = 'reels-master-download'; button.className = 'reels-master-download';
button.innerHTML = ` button.innerHTML = `
@ -380,77 +321,104 @@ class ReelsMaster {
`; `;
button.title = 'Download Reel'; button.title = 'Download Reel';
button.onclick = () => this.downloadReel(); button.onclick = () => this.downloadReel(actionContainer, button);
return button; return button;
} }
private async downloadReel(): Promise<void> { private findVideoForContainer(actionContainer: HTMLElement): HTMLVideoElement | null {
if (!this.currentVideo) { let parent = actionContainer.parentElement;
console.error('Reels Master: No video found');
while (parent) {
const video = parent.querySelector('video');
if (video) {
return video;
}
parent = parent.parentElement;
if (parent === document.body) break;
}
return this.getClosestVideoToElement(actionContainer);
}
private getClosestVideoToElement(element: HTMLElement): HTMLVideoElement | null {
const elementRect = element.getBoundingClientRect();
const elementCenterY = elementRect.top + elementRect.height / 2;
const videos = Array.from(document.querySelectorAll('video'));
let closestVideo: HTMLVideoElement | null = null;
let minDistance = Infinity;
for (const video of videos) {
const rect = video.getBoundingClientRect();
if (rect.height === 0) continue;
const videoCenter = rect.top + rect.height / 2;
const distance = Math.abs(elementCenterY - videoCenter);
if (distance < minDistance) {
minDistance = distance;
closestVideo = video;
}
}
return closestVideo;
}
private async downloadReel(actionContainer: HTMLElement, button: HTMLButtonElement): Promise<void> {
const reelUrl = window.location.href;
if (!reelUrl.includes('/reels/')) {
alert('Unable to detect reel URL');
return; return;
} }
try { try {
const videoUrl = this.currentVideo.src; button.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="reels-master-spinner">
if (!videoUrl) { <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" opacity="0.3"/>
alert('Unable to find video URL'); <path d="M12 2v4c4.42 0 8 3.58 8 8h4c0-6.63-5.37-12-12-12z"/>
return; </svg>
`;
console.log('Reels Master: Sending download request to background for', reelUrl);
const response = await chrome.runtime.sendMessage({
type: 'DOWNLOAD_REEL',
url: reelUrl
});
console.log('Reels Master: Background response', response);
if (!response.success) {
throw new Error(response.error || 'Download failed');
} }
if (this.controls.downloadButton) { button.innerHTML = `
this.controls.downloadButton.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> </svg>
</svg> `;
`;
}
const response = await fetch(videoUrl);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `reel_${Date.now()}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setTimeout(() => { setTimeout(() => {
if (this.controls.downloadButton) { button.innerHTML = `
this.controls.downloadButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
`;
}
}, 2000);
} catch (error) {
console.error('Reels Master: Download failed', error);
alert('Failed to download video. Please try again.');
if (this.controls.downloadButton) {
this.controls.downloadButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/> <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg> </svg>
`; `;
} }, 2000);
}
}
private cleanup(): void { } catch (error) {
if (this.controls.container) { console.error('Reels Master: Download failed', error);
this.controls.container.remove(); alert('Failed to download video: ' + (error instanceof Error ? error.message : 'Unknown error'));
this.controls.container = null;
this.controls.volumeSlider = null; button.innerHTML = `
this.controls.downloadButton = null; <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
`;
} }
this.currentVideo = null;
} }
} }

View File

@ -14,6 +14,6 @@
"run_at": "document_end" "run_at": "document_end"
} }
], ],
"permissions": ["storage"], "permissions": ["storage", "downloads"],
"host_permissions": ["*://*.instagram.com/*", "*://*.cdninstagram.com/*"] "host_permissions": ["*://*.instagram.com/*", "*://*.cdninstagram.com/*", "https://i.instagram.com/*", "*://*.fbcdn.net/*"]
} }