Created
March 9, 2026 14:07
-
-
Save Far-Se/417f5a0c579d2670531cc35819e990cf to your computer and use it in GitHub Desktop.
Instagram Video Controls and Video Downloader.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Instagram Media Tools | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0.0 | |
| // @description Enhanced Instagram media viewer with keyboard shortcuts and download capabilities | |
| // @author Your Name | |
| // @match https://www.instagram.com/* | |
| // @icon https://icons.duckduckgo.com/ip2/instagram.com.ico | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_openInTab | |
| // @grant GM_registerMenuCommand | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ==================== CONSTANTS ==================== | |
| const OPERATIONS = Object.freeze({ | |
| DOWNLOAD: 0, | |
| OPEN: 1, | |
| COPY: 2 | |
| }); | |
| const SELECTORS = Object.freeze({ | |
| VIDEO: 'video', | |
| REEL_VIDEO: 'div[style="width: 100%;"] > div:not([class]) a[href*="reel"] video', | |
| POST_LINK: 'a[href*="/p/"]', | |
| ARTICLE: 'article' | |
| }); | |
| const KEYBOARD_SHORTCUTS = Object.freeze({ | |
| SEEK_BACKWARD: [',', 'ArrowLeft'], | |
| SEEK_FORWARD: ['.', 'ArrowRight'], | |
| PLAY_PAUSE: [' '], | |
| OPEN_IMAGE: ['r'], | |
| OPEN_MEDIA: ['e'] | |
| }); | |
| const CONFIG = Object.freeze({ | |
| SEEK_DURATION: 1.5, | |
| PROGRESS_UPDATE_INTERVAL: 50, | |
| INIT_DELAY: 400, | |
| MIN_PROGRESS_WIDTH: 480, | |
| MAX_PROGRESS_WIDTH: 301 | |
| }); | |
| // ==================== UTILITY FUNCTIONS ==================== | |
| /** | |
| * Extracts the shortcode from the current Instagram URL | |
| * @returns {string|null} The shortcode or null if not found | |
| */ | |
| function getShortcodeFromUrl() { | |
| const pathParts = window.location.pathname.split('/'); | |
| return pathParts.at(-2) || null; | |
| } | |
| /** | |
| * Checks if an element is visible in the viewport | |
| * @param {HTMLElement} element - The element to check | |
| * @param {boolean} partiallyVisible - Whether partial visibility counts | |
| * @returns {boolean} True if element is visible | |
| */ | |
| function isElementVisibleInViewport(element, partiallyVisible = false) { | |
| const { top, left, bottom, right } = element.getBoundingClientRect(); | |
| const { innerHeight, innerWidth } = window; | |
| if (partiallyVisible) { | |
| const verticallyVisible = (top > 0 && top < innerHeight) || | |
| (bottom > 0 && bottom < innerHeight); | |
| const horizontallyVisible = (left > 0 && left < innerWidth) || | |
| (right > 0 && right < innerWidth); | |
| return verticallyVisible && horizontallyVisible; | |
| } | |
| return top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth; | |
| } | |
| /** | |
| * Extracts the best quality source from an image's srcset attribute | |
| * @param {HTMLImageElement} element - The image element | |
| * @returns {string} The best quality image URL | |
| */ | |
| function getBestSourceFromSrcset(element) { | |
| if (!element.hasAttribute('srcset')) { | |
| return element.getAttribute('src') || ''; | |
| } | |
| const srcset = element.getAttribute('srcset'); | |
| const candidates = srcset.split(',').map(item => { | |
| const [url, descriptor] = item.trim().split(/\s+/); | |
| return { url, descriptor: descriptor || '' }; | |
| }); | |
| let maxValue = 0; | |
| let bestUrl = ''; | |
| candidates.forEach(({ url, descriptor }) => { | |
| if (descriptor.endsWith('w')) { | |
| const width = parseInt(descriptor.slice(0, -1), 10); | |
| if (width > maxValue) { | |
| maxValue = width; | |
| bestUrl = url; | |
| } | |
| } else if (descriptor.endsWith('x')) { | |
| const density = parseFloat(descriptor.slice(0, -1)); | |
| if (density > maxValue) { | |
| maxValue = density; | |
| bestUrl = url; | |
| } | |
| } else if (!bestUrl) { | |
| bestUrl = url; | |
| } | |
| }); | |
| return bestUrl || element.getAttribute('src') || ''; | |
| } | |
| /** | |
| * Extracts video URL from Instagram's page HTML | |
| * @param {string} html - The HTML content to search | |
| * @returns {string|null} The video URL or null if not found | |
| */ | |
| function extractVideoUrlFromHtml(html) { | |
| try { | |
| const match = html.match(/RelayPrefetchedStreamCache.*?"url":"([^"]+mp4[^"]+)/); | |
| if (match && match[1]) { | |
| return match[1].replace(/\\+/g, ''); | |
| } | |
| } catch (error) { | |
| console.error('Error extracting video URL:', error); | |
| } | |
| return null; | |
| } | |
| /** | |
| * Gets the currently visible video element | |
| * @returns {HTMLVideoElement|null} The visible video element or null | |
| */ | |
| function getCurrentVideo() { | |
| const videos = document.querySelectorAll(SELECTORS.VIDEO); | |
| for (const video of videos) { | |
| if (isElementVisibleInViewport(video)) { | |
| return video; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Gets the last hovered element (useful for context menus) | |
| * @returns {HTMLElement|null} The hovered element or null | |
| */ | |
| function getLastHoveredElement() { | |
| const hoverElements = document.querySelectorAll(':hover'); | |
| return hoverElements[hoverElements.length - 2] || null; | |
| } | |
| // ==================== DOWNLOAD FUNCTIONS ==================== | |
| /** | |
| * Downloads a file from a URL | |
| * @param {string} url - The URL to download from | |
| * @param {string} filename - The filename to save as | |
| */ | |
| function downloadFromUrl(url, filename = 'video.mp4') { | |
| fetch(url) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| const blobUrl = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = blobUrl; | |
| link.download = filename; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(blobUrl); | |
| }) | |
| .catch(error => { | |
| console.error('Download failed:', error); | |
| handleMediaError(error); | |
| }); | |
| } | |
| /** | |
| * Handles errors by opening a fallback link | |
| * @param {Error} error - The error that occurred | |
| */ | |
| function handleMediaError(error) { | |
| console.error('Media operation failed:', error); | |
| const shortcode = getShortcodeFromUrl(); | |
| if (!shortcode) { | |
| console.error('No shortcode found in URL'); | |
| return; | |
| } | |
| const fallbackLink = `https://www.instagram.com/p/${shortcode}/#downloadVideo`; | |
| GM_openInTab(fallbackLink, { loadInBackground: true }); | |
| } | |
| // ==================== VIDEO PLAYER FUNCTIONS ==================== | |
| /** | |
| * Creates or updates a progress bar for a video element | |
| * @param {HTMLVideoElement} video - The video element | |
| */ | |
| function createOrUpdateProgressBar(video) { | |
| const videoParent = video.closest('div'); | |
| if (!videoParent) return; | |
| const rect = video.getBoundingClientRect(); | |
| let progressElement = videoParent.querySelector('progress'); | |
| if (progressElement) { | |
| progressElement.value = (video.currentTime / video.duration) * 100; | |
| return; | |
| } | |
| // Create new progress element | |
| progressElement = document.createElement('progress'); | |
| progressElement.className = 'progress progress1'; | |
| progressElement.max = 100; | |
| progressElement.value = 0; | |
| progressElement.textContent = 'Progress'; | |
| const width = rect.width < CONFIG.MAX_PROGRESS_WIDTH ? | |
| CONFIG.MIN_PROGRESS_WIDTH : rect.width; | |
| progressElement.style.width = `${width}px`; | |
| // Add click handler for seeking | |
| progressElement.onclick = (event) => { | |
| const targetVideo = event.target.closest('div').querySelector('video'); | |
| if (targetVideo) { | |
| const clickPosition = event.pageX - targetVideo.getBoundingClientRect().left; | |
| const seekPercentage = clickPosition / targetVideo.offsetWidth; | |
| targetVideo.currentTime = targetVideo.duration * seekPercentage; | |
| } | |
| }; | |
| videoParent.appendChild(progressElement); | |
| } | |
| /** | |
| * Updates progress bars for all visible videos | |
| */ | |
| function updateVideoProgress() { | |
| const videos = [...document.querySelectorAll(SELECTORS.VIDEO)].reverse(); | |
| const isReelPage = window.location.pathname.includes('reel'); | |
| videos.forEach(video => { | |
| if (isElementVisibleInViewport(video, !isReelPage)) { | |
| createOrUpdateProgressBar(video); | |
| } | |
| }); | |
| } | |
| /** | |
| * Pauses autoplay videos in the feed | |
| */ | |
| function pauseAutoplayVideos() { | |
| const autoplayVideos = document.querySelectorAll(SELECTORS.REEL_VIDEO); | |
| autoplayVideos?.forEach(video => { | |
| if (!video.paused) { | |
| video.pause(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Seeks the current video by a duration | |
| * @param {number} duration - Duration in seconds (negative for backward) | |
| */ | |
| function seekCurrentVideo(duration) { | |
| const video = getCurrentVideo(); | |
| if (video) { | |
| video.currentTime = Math.max(0, Math.min( | |
| video.currentTime + duration, | |
| video.duration | |
| )); | |
| } | |
| } | |
| /** | |
| * Toggles play/pause for the current video | |
| */ | |
| function toggleVideoPlayback() { | |
| const video = getCurrentVideo(); | |
| if (video) { | |
| if (video.paused) { | |
| video.play(); | |
| } else { | |
| video.pause(); | |
| } | |
| } | |
| } | |
| /** | |
| * Seeks to a specific percentage of the video | |
| * @param {number} percentage - Percentage (0-100) | |
| */ | |
| function seekToPercentage(percentage) { | |
| const video = getCurrentVideo(); | |
| if (video) { | |
| video.currentTime = video.duration * (percentage / 100); | |
| } | |
| } | |
| // ==================== MEDIA OPENING FUNCTIONS ==================== | |
| /** | |
| * Opens media based on the current page context | |
| * @param {number} operation - The operation type (DOWNLOAD, OPEN, COPY) | |
| */ | |
| function openMedia(operation = OPERATIONS.OPEN) { | |
| const pathname = window.location.pathname; | |
| const href = window.location.href; | |
| // Home feed | |
| if (pathname === '/') { | |
| openMediaFromFeed(); | |
| return; | |
| } | |
| // Post or Reel page | |
| if (href.includes('/p/') || href.includes('/reel/')) { | |
| openVideoFromPost(); | |
| return; | |
| } | |
| // Stories | |
| if (href.includes('stories')) { | |
| openImageFromStories(); | |
| return; | |
| } | |
| // Reels feed | |
| if (href.includes('reels')) { | |
| openVideoFromReels(operation); | |
| return; | |
| } | |
| // Fallback | |
| openImageFromHover() || openVideoFromReels(operation); | |
| } | |
| /** | |
| * Opens media from the home feed | |
| */ | |
| function openMediaFromFeed() { | |
| try { | |
| const hoveredArticle = getLastHoveredElement()?.closest(SELECTORS.ARTICLE); | |
| if (!hoveredArticle) return; | |
| let image = hoveredArticle.querySelector('div>img[alt^="Photo by"]'); | |
| if(image) | |
| { | |
| openFromPointer(); | |
| return; | |
| } | |
| const postLinks = hoveredArticle.querySelectorAll(SELECTORS.POST_LINK); | |
| const lastLink = [...postLinks].at(-1); | |
| if (!lastLink) return; | |
| const shortcode = lastLink.getAttribute('href').split('/').at(2); | |
| const fallbackLink = `https://www.instagram.com/p/${shortcode}#downloadVideo`; | |
| window.open(fallbackLink, '_blank'); | |
| } catch (error) { | |
| console.error('Failed to open media from feed:', error); | |
| } | |
| } | |
| let lastMouseX = 0; | |
| let lastMouseY = 0; | |
| // Track mouse position globally | |
| document.addEventListener('mousemove', (e) => { | |
| lastMouseX = e.clientX; | |
| lastMouseY = e.clientY; | |
| }, true); | |
| function openFromPointer(){ | |
| // Get all elements at the last mouse position | |
| const elements = document.elementsFromPoint(lastMouseX, lastMouseY); | |
| if (!elements || elements.length === 0) { | |
| console.log('No elements found at mouse position'); | |
| return; | |
| } | |
| // Search through elements to find media | |
| for (const el of elements) { | |
| let mediaUrl = null; | |
| // Check for IMG elements | |
| if (el.tagName === 'IMG') { | |
| mediaUrl = el.src || el.dataset.src || el.dataset.lazySrc; | |
| } | |
| // Check for VIDEO elements | |
| else if (el.tagName === 'VIDEO') { | |
| if(!el.src.startsWith('blob:')) | |
| { | |
| mediaUrl = el.src || el.currentSrc || (el.querySelector('source')?.src); | |
| }else { | |
| let article = el.closest('article'); | |
| if(article) | |
| { | |
| let href = article.querySelector("a[role='link'][href*='/p']"); | |
| if(href) mediaUrl = href.replace('liked_by/','')+'#downloadVideo'; | |
| } | |
| } | |
| } | |
| // Check for elements with background images | |
| else if (el.style.backgroundImage) { | |
| const bgMatch = el.style.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/); | |
| if (bgMatch) { | |
| mediaUrl = bgMatch[1]; | |
| } | |
| } | |
| // Check for A tags linking to images/videos | |
| else if (el.tagName === 'A') { | |
| const href = el.href; | |
| if (href && /\.(jpg|jpeg|png|gif|webp|mp4|webm|mov)(\?|$)/i.test(href)) { | |
| mediaUrl = href; | |
| } | |
| } | |
| // Check for common data attributes | |
| else if (el.dataset.fullImage || el.dataset.fullSrc) { | |
| mediaUrl = el.dataset.fullImage || el.dataset.fullSrc; | |
| } | |
| // If media found, open it | |
| if (mediaUrl) { | |
| console.log('Found media:', mediaUrl); | |
| window.open(mediaUrl, '_blank'); | |
| return; | |
| } | |
| } | |
| } | |
| /** | |
| * Opens video from a post page | |
| */ | |
| function openVideoFromPost() { | |
| try { | |
| const url = extractVideoUrlFromHtml(document.body.innerHTML); | |
| if (url) { | |
| window.open(url, '_blank'); | |
| } else { | |
| throw new Error('Video URL not found'); | |
| } | |
| } catch (error) { | |
| window.open(window.location.href + `#downloadVideo`, '_blank'); | |
| console.error('Failed to open video from post:', error); | |
| } | |
| } | |
| /** | |
| * Opens image from stories | |
| */ | |
| function openImageFromStories() { | |
| const hoveredElement = getLastHoveredElement(); | |
| const imageElement = hoveredElement?.querySelector('img'); | |
| const href = imageElement?.getAttribute('src'); | |
| if (href) { | |
| window.open(href, '_blank'); | |
| } | |
| } | |
| /** | |
| * Opens video from reels | |
| * @param {number} operation - The operation type | |
| */ | |
| function openVideoFromReels(operation) { | |
| handleMediaError(new Error('Reel video extraction')); | |
| } | |
| /** | |
| * Opens image from hovered element | |
| * @returns {boolean} Success status | |
| */ | |
| function openImageFromHover() { | |
| try { | |
| const hoveredElement = getLastHoveredElement(); | |
| const imageElement = hoveredElement?.querySelector('img'); | |
| if (!imageElement) return false; | |
| const href = getBestSourceFromSrcset(imageElement); | |
| if (href) { | |
| window.open(href, '_blank'); | |
| return true; | |
| } | |
| } catch (error) { | |
| console.error('Failed to open image from hover:', error); | |
| } | |
| return false; | |
| } | |
| // ==================== KEYBOARD HANDLER ==================== | |
| /** | |
| * Checks if user is currently typing in an input field | |
| * @returns {boolean} True if input is focused | |
| */ | |
| function isInputFocused() { | |
| const activeElement = document.activeElement; | |
| if (activeElement?.getAttribute('aria-label')?.includes('comment')) { | |
| return true; | |
| } | |
| return document.querySelector('input:focus') !== null || | |
| document.querySelector('textarea:focus') !== null; | |
| } | |
| /** | |
| * Handles keyboard shortcuts | |
| * @param {KeyboardEvent} event - The keyboard event | |
| */ | |
| function handleKeyboardShortcut(event) { | |
| if (isInputFocused()) return; | |
| const key = event.key; | |
| const keyNumber = parseInt(key); | |
| // Number keys for seeking to percentage | |
| if (keyNumber > 0 && keyNumber <= 9) { | |
| seekToPercentage(keyNumber * 10); | |
| return; | |
| } | |
| // Arrow keys and comma/period for seeking | |
| if (KEYBOARD_SHORTCUTS.SEEK_BACKWARD.includes(key)) { | |
| seekCurrentVideo(-CONFIG.SEEK_DURATION); | |
| return; | |
| } | |
| if (KEYBOARD_SHORTCUTS.SEEK_FORWARD.includes(key)) { | |
| seekCurrentVideo(CONFIG.SEEK_DURATION); | |
| return; | |
| } | |
| // Space for play/pause | |
| if (KEYBOARD_SHORTCUTS.PLAY_PAUSE.includes(key)) { | |
| if (window.location.href.includes('stories')) return; | |
| event.preventDefault(); | |
| toggleVideoPlayback(); | |
| return; | |
| } | |
| // 'R' key for opening images | |
| if (key === 'r' && !event.shiftKey && !event.ctrlKey && | |
| !event.altKey && !event.metaKey) { | |
| openImageFromHover(); | |
| return; | |
| } | |
| // 'E' key for opening media | |
| if (key === 'e' && !event.ctrlKey) { | |
| openMedia(OPERATIONS.OPEN); | |
| return; | |
| } | |
| } | |
| // ==================== HASH HANDLER ==================== | |
| /** | |
| * Handles download video hash in URL | |
| */ | |
| function handleDownloadVideoHash() { | |
| if (!window.location.hash.includes('downloadVideo')) return; | |
| try { | |
| const url = extractVideoUrlFromHtml(document.body.innerHTML); | |
| if (url) { | |
| GM_openInTab(url, { loadInBackground: false }); | |
| setTimeout(() => window.close(), 100); | |
| } | |
| } catch (error) { | |
| console.error('Failed to handle download hash:', error); | |
| } | |
| } | |
| // ==================== INITIALIZATION ==================== | |
| /** | |
| * Initializes the userscript | |
| */ | |
| function initialize() { | |
| // Handle download hash immediately | |
| handleDownloadVideoHash(); | |
| // Set up periodic tasks | |
| setInterval(() => { | |
| if (document.visibilityState !== 'visible') return; | |
| updateVideoProgress(); | |
| pauseAutoplayVideos(); | |
| }, CONFIG.PROGRESS_UPDATE_INTERVAL); | |
| // Set up keyboard shortcuts | |
| document.addEventListener('keydown', handleKeyboardShortcut, { passive: true }); | |
| // Register menu command | |
| GM_registerMenuCommand('Open Media', () => { | |
| openMedia(OPERATIONS.OPEN); | |
| }, 'm'); | |
| console.log('Instagram Media Tools initialized'); | |
| } | |
| // Start after a short delay to ensure page is ready | |
| setTimeout(initialize, CONFIG.INIT_DELAY); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment