diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 9698392..6943c1c 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -1,7 +1,173 @@ // Background Service Worker for Reels Master 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(() => { 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): 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; +} diff --git a/src/content/content.css b/src/content/content.css index 086f37d..27719cb 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -193,3 +193,17 @@ .reels-master-download:hover::after { opacity: 1; } + +/* Spinner animation for download */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.reels-master-spinner { + animation: spin 1s linear infinite; +} diff --git a/src/content/content.ts b/src/content/content.ts index e8661c1..d4e7adc 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -2,31 +2,21 @@ import './content.css'; console.log('Reels Master: Content script loaded'); -interface ReelsControls { - volumeSlider: HTMLInputElement | null; - downloadButton: HTMLButtonElement | null; - container: HTMLDivElement | null; -} - 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 storedMuted: boolean = false; - private videoVolumeListeners: WeakMap void> = new WeakMap(); + private processedContainers: WeakSet = new WeakSet(); + private videoVolumeListeners: WeakMap = new WeakMap(); + private domObserver: MutationObserver | null = null; constructor() { this.init(); } private init(): void { - // Сразу начинаем следить за всеми видео для мгновенного применения громкости - this.setupGlobalVideoInterceptor(); + this.loadSettings(); + + this.setupVideoInterceptor(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.start()); @@ -35,25 +25,47 @@ class ReelsMaster { } } - // Перехватываем все видео сразу при их появлении и применяем сохраненную громкость - private setupGlobalVideoInterceptor(): void { - // Применяем к уже существующим видео + private loadSettings(): 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(); - // Следим за новыми видео через MutationObserver const videoObserver = new MutationObserver((mutations) => { - let hasNewVideo = false; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof HTMLVideoElement) { - hasNewVideo = true; this.applyVolumeToVideo(node); } else if (node instanceof HTMLElement) { const videos = node.querySelectorAll('video'); - if (videos.length > 0) { - hasNewVideo = true; - videos.forEach(video => this.applyVolumeToVideo(video)); - } + videos.forEach(video => this.applyVolumeToVideo(video)); } } } @@ -66,6 +78,8 @@ class ReelsMaster { } private applyVolumeToAllVideos(): void { + if (!window.location.pathname.includes('/reels/')) return; + document.querySelectorAll('video').forEach(video => { this.applyVolumeToVideo(video); }); @@ -74,202 +88,135 @@ class ReelsMaster { private applyVolumeToVideo(video: HTMLVideoElement): void { if (!window.location.pathname.includes('/reels/')) return; - // Применяем сохраненную громкость video.volume = this.storedVolume; video.muted = this.storedMuted; - // Добавляем слушатель на случай если Instagram перезапишет громкость if (!this.videoVolumeListeners.has(video)) { - const listener = () => { - // Если громкость изменилась не нами, восстанавливаем - if (Math.abs(video.volume - this.storedVolume) > 0.01 || video.muted !== this.storedMuted) { + this.videoVolumeListeners.set(video, true); + + let changeCount = 0; + const maxChanges = 10; + + const enforceVolume = () => { + changeCount++; + if (changeCount <= maxChanges) { video.volume = this.storedVolume; video.muted = this.storedMuted; } }; - - // Слушаем первые несколько изменений громкости для борьбы с Instagram - let volumeChangeCount = 0; - const tempListener = () => { - volumeChangeCount++; - 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); + + video.addEventListener('volumechange', enforceVolume); + video.addEventListener('loadedmetadata', enforceVolume); + video.addEventListener('play', enforceVolume); + video.addEventListener('canplay', enforceVolume); } } - private start(): void { - console.log('Reels Master: Starting...'); - this.checkForReels(); - this.observeUrlChanges(); - this.observeDOMChanges(); - window.addEventListener('scroll', () => { - this.checkForReels(); - }, { passive: true }); - } - - private observeUrlChanges(): void { - let lastUrl = location.href; - new MutationObserver(() => { - const currentUrl = location.href; - if (currentUrl !== lastUrl) { - lastUrl = currentUrl; - console.log('Reels Master: URL changed to', currentUrl); - // Сразу применяем громкость ко всем видео - this.applyVolumeToAllVideos(); - setTimeout(() => this.checkForReels(), 300); + private setupDOMObserver(): void { + this.domObserver = new MutationObserver((mutations) => { + if (!window.location.pathname.includes('/reels/')) return; + + let shouldCheck = false; + + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement) { + if (node.querySelector('video') || node.querySelector('svg[aria-label="Like"]')) { + shouldCheck = true; + break; + } + } + } + if (shouldCheck) break; + } + + if (shouldCheck) { + 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, subtree: true }); } - private checkForReels(): void { - if (!window.location.pathname.includes('/reels/')) { - this.cleanup(); - return; - } + private injectControlsToAllContainers(): void { + if (!window.location.pathname.includes('/reels/')) return; - const video = this.getActiveVideo(); - if (!video || video === this.currentVideo) { - return; - } + const actionContainers = this.findAllActionContainers(); + + console.log(`Reels Master: Found ${actionContainers.length} action containers`); - console.log('Reels Master: Found new video element'); - this.currentVideo = video; - - // Убедимся что громкость применена к текущему видео - this.applyVolumeToVideo(video); - - this.injectControls(); + for (const container of actionContainers) { + this.injectControlsToContainer(container); + } } - private getActiveVideo(): HTMLVideoElement | null { - const videos = Array.from(document.querySelectorAll('video')); - if (videos.length === 0) return null; - - const center = window.innerHeight / 2; - 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(center - videoCenter); - - if (distance < minDistance) { - minDistance = distance; - closestVideo = video; + private findAllActionContainers(): HTMLElement[] { + const containers: HTMLElement[] = []; + + const likeButtons = document.querySelectorAll('svg[aria-label="Like"]'); + + for (const likeButton of likeButtons) { + const container = this.findActionContainerFromLikeButton(likeButton); + if (container && !containers.includes(container)) { + containers.push(container); } } - - 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) { - // Пробуем найти родителя вверх по дереву - let parent = video.parentElement; - for (let i = 0; i < 10 && parent; i++) { - if (parent.querySelector('svg[aria-label="Like"]')) { - reelContainer = parent; - break; + return containers; + } + + private findActionContainerFromLikeButton(likeButton: Element): HTMLElement | null { + let parent = likeButton.parentElement; + + 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; } + 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 { const container = document.createElement('div'); container.className = 'reels-master-controls'; @@ -280,32 +227,23 @@ class ReelsMaster { const volumeControl = document.createElement('div'); volumeControl.className = 'reels-master-volume'; - const volumeButton = document.createElement('button'); - volumeButton.className = 'reels-master-volume-button'; - volumeButton.innerHTML = ` - - - - `; - const sliderContainer = document.createElement('div'); sliderContainer.className = 'reels-master-slider-container'; - this.controls.volumeSlider = this.createVolumeSlider(); - sliderContainer.appendChild(this.controls.volumeSlider); - - volumeButton.onclick = () => { - if (this.currentVideo) { - this.storedMuted = !this.storedMuted; - - // Применяем ко всем видео - this.applyVolumeToAllVideos(); + const slider = this.createVolumeSlider(); + sliderContainer.appendChild(slider); - if (this.controls.volumeSlider) { - this.controls.volumeSlider.value = this.storedMuted ? '0' : String(this.storedVolume * 100); - } - this.updateVolumeIcon(volumeButton); - } + const volumeButton = document.createElement('button'); + volumeButton.className = 'reels-master-volume-button'; + this.updateVolumeIcon(volumeButton); + + volumeButton.onclick = () => { + this.storedMuted = !this.storedMuted; + + this.saveSettings(); + this.applyVolumeToAllVideos(); + this.updateAllSliders(); + this.updateAllVolumeIcons(); }; volumeControl.appendChild(sliderContainer); @@ -319,36 +257,39 @@ class ReelsMaster { slider.type = 'range'; slider.min = '0'; slider.max = '100'; - - // Используем сохраненную громкость - slider.value = String(this.storedVolume * 100); - + slider.value = this.storedMuted ? '0' : String(this.storedVolume * 100); slider.className = 'reels-master-volume-slider'; slider.oninput = (e) => { - if (this.currentVideo) { - const value = parseInt((e.target as HTMLInputElement).value); - const newVolume = value / 100; - const newMuted = value === 0; - - // Сохраняем настройки - this.storedVolume = newVolume; - this.storedMuted = newMuted; - - // Применяем ко всем видео сразу - this.applyVolumeToAllVideos(); - - const volumeControl = slider.closest('.reels-master-volume'); - const volumeButton = volumeControl?.querySelector('.reels-master-volume-button') as HTMLButtonElement; - if (volumeButton) { - this.updateVolumeIcon(volumeButton); - } - } + const value = parseInt((e.target as HTMLInputElement).value); + this.storedVolume = value / 100; + this.storedMuted = value === 0; + + this.saveSettings(); + this.applyVolumeToAllVideos(); + this.updateAllSliders(); + this.updateAllVolumeIcons(); }; return slider; } + private updateAllSliders(): void { + const sliders = document.querySelectorAll('.reels-master-volume-slider') as NodeListOf; + 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; + buttons.forEach(button => { + this.updateVolumeIcon(button); + }); + } + private updateVolumeIcon(button: HTMLButtonElement): void { const volume = this.storedMuted ? 0 : this.storedVolume; @@ -370,7 +311,7 @@ class ReelsMaster { button.innerHTML = icon; } - private createDownloadButton(): HTMLButtonElement { + private createDownloadButton(actionContainer: HTMLElement): HTMLButtonElement { const button = document.createElement('button'); button.className = 'reels-master-download'; button.innerHTML = ` @@ -380,77 +321,104 @@ class ReelsMaster { `; button.title = 'Download Reel'; - button.onclick = () => this.downloadReel(); + button.onclick = () => this.downloadReel(actionContainer, button); return button; } - private async downloadReel(): Promise { - if (!this.currentVideo) { - console.error('Reels Master: No video found'); + private findVideoForContainer(actionContainer: HTMLElement): HTMLVideoElement | null { + let parent = actionContainer.parentElement; + + 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 { + const reelUrl = window.location.href; + + if (!reelUrl.includes('/reels/')) { + alert('Unable to detect reel URL'); return; } try { - const videoUrl = this.currentVideo.src; - - if (!videoUrl) { - alert('Unable to find video URL'); - return; + button.innerHTML = ` + + + + + `; + + 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) { - this.controls.downloadButton.innerHTML = ` - - - - `; - } - - 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); + button.innerHTML = ` + + + + `; setTimeout(() => { - if (this.controls.downloadButton) { - this.controls.downloadButton.innerHTML = ` - - - - `; - } - }, 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 = ` + button.innerHTML = ` `; - } - } - } + }, 2000); - private cleanup(): void { - if (this.controls.container) { - this.controls.container.remove(); - this.controls.container = null; - this.controls.volumeSlider = null; - this.controls.downloadButton = null; + } catch (error) { + console.error('Reels Master: Download failed', error); + alert('Failed to download video: ' + (error instanceof Error ? error.message : 'Unknown error')); + + button.innerHTML = ` + + + + `; } - this.currentVideo = null; } } diff --git a/src/manifest.json b/src/manifest.json index c215f5b..fcbc16c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -14,6 +14,6 @@ "run_at": "document_end" } ], - "permissions": ["storage"], - "host_permissions": ["*://*.instagram.com/*", "*://*.cdninstagram.com/*"] + "permissions": ["storage", "downloads"], + "host_permissions": ["*://*.instagram.com/*", "*://*.cdninstagram.com/*", "https://i.instagram.com/*", "*://*.fbcdn.net/*"] } \ No newline at end of file