├── images ├── config.png ├── promo.png ├── netflix-skipper-128.png ├── netflix-skipper-16.png ├── netflix-skipper-32.png └── netflix-skipper-48.png ├── src ├── background.js ├── content_script.js ├── popup.html └── popup.js ├── README.md ├── manifest.json └── LICENSE /images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/config.png -------------------------------------------------------------------------------- /images/promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/promo.png -------------------------------------------------------------------------------- /images/netflix-skipper-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/netflix-skipper-128.png -------------------------------------------------------------------------------- /images/netflix-skipper-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/netflix-skipper-16.png -------------------------------------------------------------------------------- /images/netflix-skipper-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/netflix-skipper-32.png -------------------------------------------------------------------------------- /images/netflix-skipper-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/netflix-skipper/HEAD/images/netflix-skipper-48.png -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(() => { 2 | chrome.storage.local.set({ skipIntro: true, skipRecap: true, skipNext: true }); 3 | }); 4 | 5 | if (chrome.runtime) { 6 | chrome.runtime.setUninstallURL("https://docs.google.com/forms/d/e/1FAIpQLSdu75DIgQwiEYud7I8TgsAkyoUnFyLXRDAnELcN_QIeLGvh5w/viewform"); 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netflix-skipper 2 | Automatically skip Netflix intros, recaps, and next episode prompts ⚡⌛ 3 | 4 | ![Promo](images/promo.png) 5 | 6 | Don't waste any more time waiting or clicking on intros, recaps, and next episode buttons. 7 | 8 | Once the extension is installed, reload or open a Netflix Tab, and it will work automatically. Through the popup, you can configure which events (intros, recaps, and next episode) you would like to skip. To exclude specific shows from your configured auto-skip settings, you can add the current show's title to an exemption list - or remove it from the list to restore the automatic skipping behavior. 9 | 10 | ![Config](images/config.png) 11 | 12 | This extension supports ALL Netflix languages! 13 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Netflix Skipper", 3 | "description": "Automatically skip Netflix intros, recaps, and next episode prompts.", 4 | "author": "Ran Ribenzaft", 5 | "version": "1.0", 6 | "manifest_version": 3, 7 | "background": { 8 | "service_worker": "src/background.js" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "*://*/*" 14 | ], 15 | "js": ["src/content_script.js"], 16 | "run_at": "document_end" 17 | } 18 | ], 19 | "permissions": ["storage", "activeTab"], 20 | "action": { 21 | "default_popup": "src/popup.html", 22 | "default_icon": { 23 | "16": "images/netflix-skipper-16.png", 24 | "32": "images/netflix-skipper-32.png", 25 | "48": "images/netflix-skipper-48.png", 26 | "128": "images/netflix-skipper-128.png" 27 | } 28 | }, 29 | "icons": { 30 | "16": "images/netflix-skipper-16.png", 31 | "32": "images/netflix-skipper-32.png", 32 | "48": "images/netflix-skipper-48.png", 33 | "128": "images/netflix-skipper-128.png" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ran Ribenzaft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/content_script.js: -------------------------------------------------------------------------------- 1 | const INTRO_UIA = "player-skip-intro"; 2 | const RECAP_UIA = "player-skip-recap"; 3 | const NEXT_UIA = "next-episode-seamless-button"; 4 | const NEXT_DRAIN_UIA = "next-episode-seamless-button-draining"; 5 | 6 | const BUTTONS = [INTRO_UIA, RECAP_UIA, NEXT_UIA, NEXT_DRAIN_UIA]; 7 | 8 | // Function to extract the current Netflix title 9 | function getCurrentTitle() { 10 | // Use the specific selector that works reliably 11 | const titleElement = document.querySelectorAll("[data-uia='video-title']")[0]; 12 | 13 | if (titleElement) { 14 | // Try to get the show title from the h4 element within the title element 15 | const h4Element = titleElement.querySelector('h4'); 16 | if (h4Element && h4Element.textContent.trim()) { 17 | return h4Element.textContent.trim(); 18 | } 19 | 20 | // Fallback to full text content if no h4 found 21 | if (titleElement.textContent.trim()) { 22 | return titleElement.textContent.trim(); 23 | } 24 | } 25 | 26 | // Fallback: try to get title from page title if the main selector fails 27 | const pageTitle = document.title; 28 | if (pageTitle && pageTitle !== 'Netflix' && !pageTitle.includes('Watch ')) { 29 | return pageTitle.replace(' - Netflix', '').trim(); 30 | } 31 | 32 | return null; 33 | } 34 | 35 | async function skipper() { 36 | try { 37 | chrome.storage.local.get( 38 | ["skipIntro", "skipRecap", "skipNext", "exemptTitles"], 39 | ({ skipIntro, skipRecap, skipNext, exemptTitles = [] }) => { 40 | // Check if current title is in exempt list 41 | const currentTitle = getCurrentTitle(); 42 | const isExempt = currentTitle && exemptTitles.includes(currentTitle); 43 | 44 | // If title is exempt, don't skip anything 45 | if (isExempt) { 46 | return; 47 | } 48 | 49 | const mapper = { 50 | [INTRO_UIA]: skipIntro, 51 | [RECAP_UIA]: skipRecap, 52 | [NEXT_UIA]: skipNext, 53 | [NEXT_DRAIN_UIA]: skipNext, 54 | }; 55 | BUTTONS.forEach((uia) => { 56 | const button = Object.values( 57 | document.getElementsByTagName("button") 58 | ).find((elem) => elem.getAttribute("data-uia") === uia); 59 | if (button && mapper[uia]) { 60 | button.click(); 61 | } 62 | }); 63 | } 64 | ); 65 | } catch (err) { 66 | console.error(err); 67 | } 68 | } 69 | 70 | // Function to add/remove current title from exempt list 71 | async function toggleExemptStatus() { 72 | const currentTitle = getCurrentTitle(); 73 | if (!currentTitle) { 74 | console.log('Netflix Skipper: Could not detect current title'); 75 | return; 76 | } 77 | 78 | try { 79 | const result = await chrome.storage.local.get(['exemptTitles']); 80 | const exemptTitles = result.exemptTitles || []; 81 | 82 | if (exemptTitles.includes(currentTitle)) { 83 | // Remove from exempt list 84 | const updatedTitles = exemptTitles.filter(title => title !== currentTitle); 85 | await chrome.storage.local.set({ exemptTitles: updatedTitles }); 86 | console.log(`Netflix Skipper: Removed "${currentTitle}" from exempt list`); 87 | } else { 88 | // Add to exempt list 89 | const updatedTitles = [...exemptTitles, currentTitle]; 90 | await chrome.storage.local.set({ exemptTitles: updatedTitles }); 91 | console.log(`Netflix Skipper: Added "${currentTitle}" to exempt list`); 92 | } 93 | } catch (err) { 94 | console.error('Netflix Skipper: Error toggling exempt status:', err); 95 | } 96 | } 97 | 98 | // Listen for messages from popup 99 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 100 | if (request.action === 'toggleExempt') { 101 | toggleExemptStatus(); 102 | sendResponse({ success: true }); 103 | } else if (request.action === 'getCurrentTitle') { 104 | const title = getCurrentTitle(); 105 | sendResponse({ title }); 106 | } 107 | }); 108 | 109 | if (document.location.host.includes(".netflix.")) { 110 | setInterval(() => skipper(), 500); 111 | } 112 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 106 | 107 | 108 |
109 |
General Settings
110 |
111 | 112 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 |
123 |
124 | Playing: Loading... 125 |
126 |
127 |
Exempt List
128 | 134 | 139 |
140 |
Exempt Titles:
141 |
142 |
No exempt titles
143 |
144 |
145 |
146 | 147 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | let skipIntroCheckbox = document.getElementById("skip-intro"); 2 | let skipRecapCheckbox = document.getElementById("skip-recap"); 3 | let skipNextCheckbox = document.getElementById("skip-next"); 4 | let toggleExemptButton = document.getElementById("toggle-exempt"); 5 | let currentTitleSpan = document.getElementById("current-title"); 6 | let currentTitleContainer = document.getElementById("current-title-container"); 7 | let notOnNetflixDiv = document.getElementById("not-on-netflix"); 8 | let exemptListDiv = document.getElementById("exempt-list"); 9 | 10 | let currentTitle = null; 11 | let exemptTitles = []; 12 | 13 | // General settings event listeners 14 | skipIntroCheckbox.addEventListener("click", async () => { 15 | chrome.storage.local.set({ skipIntro: skipIntroCheckbox.checked }); 16 | }); 17 | 18 | skipRecapCheckbox.addEventListener("click", async () => { 19 | chrome.storage.local.set({ skipRecap: skipRecapCheckbox.checked }); 20 | }); 21 | 22 | skipNextCheckbox.addEventListener("click", async () => { 23 | chrome.storage.local.set({ skipNext: skipNextCheckbox.checked }); 24 | }); 25 | 26 | // Exempt list functionality 27 | toggleExemptButton.addEventListener("click", async () => { 28 | if (!currentTitle) return; 29 | 30 | try { 31 | // Toggle the exempt status locally for immediate UI update 32 | const isCurrentlyExempt = exemptTitles.includes(currentTitle); 33 | 34 | if (isCurrentlyExempt) { 35 | // Remove from exempt list 36 | exemptTitles = exemptTitles.filter(title => title !== currentTitle); 37 | } else { 38 | // Add to exempt list 39 | exemptTitles = [...exemptTitles, currentTitle]; 40 | } 41 | 42 | // Update storage 43 | await chrome.storage.local.set({ exemptTitles }); 44 | 45 | // Send message to content script to sync the change 46 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 47 | await chrome.tabs.sendMessage(tab.id, { action: 'toggleExempt' }); 48 | 49 | // Update UI immediately 50 | updateExemptList(); 51 | updateToggleButton(); 52 | } catch (error) { 53 | console.error('Error toggling exempt status:', error); 54 | // If there's an error, reload from storage to ensure consistency 55 | await loadExemptTitles(); 56 | updateToggleButton(); 57 | } 58 | }); 59 | 60 | // Function to load exempt titles from storage 61 | async function loadExemptTitles() { 62 | try { 63 | const result = await chrome.storage.local.get(['exemptTitles']); 64 | exemptTitles = result.exemptTitles || []; 65 | updateExemptList(); 66 | } catch (error) { 67 | console.error('Error loading exempt titles:', error); 68 | } 69 | } 70 | 71 | // Function to update the exempt list display 72 | function updateExemptList() { 73 | if (exemptTitles.length === 0) { 74 | exemptListDiv.innerHTML = '
No exempt titles
'; 75 | return; 76 | } 77 | 78 | // Clear the container first 79 | exemptListDiv.innerHTML = ''; 80 | 81 | // Create each exempt item with proper event listeners 82 | exemptTitles.forEach((title, index) => { 83 | const itemDiv = document.createElement('div'); 84 | itemDiv.className = 'exempt-item'; 85 | 86 | const titleSpan = document.createElement('span'); 87 | titleSpan.style.flex = '1'; 88 | titleSpan.style.marginRight = '8px'; 89 | titleSpan.textContent = title; 90 | 91 | const removeButton = document.createElement('button'); 92 | removeButton.className = 'remove-item'; 93 | removeButton.textContent = 'X'; 94 | removeButton.title = `Remove "${title}" from exempt list`; 95 | removeButton.addEventListener('click', () => removeExemptTitle(title)); 96 | 97 | itemDiv.appendChild(titleSpan); 98 | itemDiv.appendChild(removeButton); 99 | exemptListDiv.appendChild(itemDiv); 100 | }); 101 | } 102 | 103 | // Function to remove a title from exempt list 104 | window.removeExemptTitle = async function(title) { 105 | try { 106 | // Update local array immediately for instant UI feedback 107 | exemptTitles = exemptTitles.filter(t => t !== title); 108 | 109 | // Update storage 110 | await chrome.storage.local.set({ exemptTitles }); 111 | 112 | // Update UI immediately 113 | updateExemptList(); 114 | updateToggleButton(); 115 | 116 | // Visual feedback - briefly highlight the change 117 | if (title === currentTitle) { 118 | toggleExemptButton.style.transform = 'scale(0.95)'; 119 | setTimeout(() => { 120 | toggleExemptButton.style.transform = 'scale(1)'; 121 | }, 150); 122 | } 123 | } catch (error) { 124 | console.error('Error removing exempt title:', error); 125 | // If there's an error, reload from storage to ensure consistency 126 | await loadExemptTitles(); 127 | updateToggleButton(); 128 | } 129 | }; 130 | 131 | // Function to update the toggle button state 132 | function updateToggleButton() { 133 | if (!currentTitle) { 134 | toggleExemptButton.textContent = 'No title detected'; 135 | toggleExemptButton.className = 'toggle-button disabled-button'; 136 | toggleExemptButton.disabled = true; 137 | return; 138 | } 139 | 140 | const isExempt = exemptTitles.includes(currentTitle); 141 | if (isExempt) { 142 | toggleExemptButton.textContent = 'Remove from Exempt List'; 143 | toggleExemptButton.className = 'toggle-button remove-button'; 144 | toggleExemptButton.disabled = false; 145 | } else { 146 | toggleExemptButton.textContent = 'Add to Exempt List'; 147 | toggleExemptButton.className = 'toggle-button add-button'; 148 | toggleExemptButton.disabled = false; 149 | } 150 | } 151 | 152 | // Function to get current title from active tab 153 | async function getCurrentTitle() { 154 | try { 155 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 156 | 157 | // Check if we're on Netflix 158 | if (!tab.url.includes('netflix.com')) { 159 | currentTitleContainer.style.display = 'none'; 160 | notOnNetflixDiv.style.display = 'block'; 161 | return; 162 | } 163 | 164 | // Show the current title container 165 | currentTitleContainer.style.display = 'block'; 166 | notOnNetflixDiv.style.display = 'none'; 167 | 168 | // Get title from content script 169 | const response = await chrome.tabs.sendMessage(tab.id, { action: 'getCurrentTitle' }); 170 | currentTitle = response?.title || null; 171 | 172 | if (currentTitle) { 173 | currentTitleSpan.textContent = currentTitle; 174 | } else { 175 | currentTitleSpan.textContent = 'Title not detected'; 176 | } 177 | 178 | updateToggleButton(); 179 | } catch (error) { 180 | console.error('Error getting current title:', error); 181 | currentTitleContainer.style.display = 'none'; 182 | notOnNetflixDiv.style.display = 'block'; 183 | } 184 | } 185 | 186 | // Initialize popup 187 | async function initializePopup() { 188 | // Load general settings 189 | chrome.storage.local.get( 190 | ["skipIntro", "skipRecap", "skipNext"], 191 | ({ skipIntro, skipRecap, skipNext }) => { 192 | if (skipIntro) { 193 | skipIntroCheckbox.checked = true; 194 | } 195 | if (skipRecap) { 196 | skipRecapCheckbox.checked = true; 197 | } 198 | if (skipNext) { 199 | skipNextCheckbox.checked = true; 200 | } 201 | } 202 | ); 203 | 204 | // Load exempt titles and current title 205 | await loadExemptTitles(); 206 | await getCurrentTitle(); 207 | } 208 | 209 | // Initialize when popup opens 210 | initializePopup(); 211 | --------------------------------------------------------------------------------