SkipAd is a J1 Template module that lets you watch YouTube videos completely free of advertisements. It wraps the VideoJS player around the YouTube IFrame API so that every pre-roll, mid-roll, and overlay ad is silently blocked — no browser plugin, no paid subscription required.
Beyond simple ad blocking, SkipAd maintains a personal playlist stored inside your own browser, lets you sort and search your watch history, and supports optional loop playback and Picture-in-Picture mode for multitasking.
20-30 Minutes to read
SkipAd is built as a self-contained JavaScript module (UMD) that exports a single global object named skipAd. All user-facing features — video loading, playlist management, search, sort, and I/O — are wired up automatically when the J1 adapter initialises the module.
Key features and benefits:
- Ad-free playback
-
Every YouTube ad format (pre-roll, mid-roll, overlay) is blocked automatically by routing playback through the VideoJS YouTube tech instead of the normal YouTube player. No manual intervention is needed.
- Personal playlist
-
Every video you watch is added to a browser-local playlist stored in
localStorage. The list survives page reloads and browser restarts as long as you do not clear your browser data. - Multiple display modes
-
Your playlist can be displayed as visual Cards (default, showing a thumbnail, title, and metadata at a glance) or as a compact List view suited to longer histories.
- Sorting and searching
-
The playlist can be sorted by watch date, issue date, duration, title, author, category, or type. A full-text search powered by the Lunr.js engine lets you filter entries by any combination of title, author, category, and tags.
- Import and export
-
Your playlist can be exported to a JSON file for backup or sharing and re-imported at any time. A set of hand-picked server playlists is also available for one-click import.
- Loop mode
-
When loop mode is enabled, SkipAd automatically advances to the next video in your playlist when the current one ends — following whichever sort order is currently active.
- Picture-in-Picture (PiP)
-
When your browser supports the Document Picture-in-Picture API, a PiP button appears in the player control bar. Clicking it opens the video in a floating window so you can keep watching while working in other tabs.
- Resume playback
-
SkipAd saves your last playback position for every video. The next time you open the same video it will resume from where you left off instead of starting from the beginning.
How SkipAd Works
Understanding the basic flow helps you get the most out of SkipAd. When you paste a YouTube link and click Watch Video, the following steps happen:
-
The module extracts the 11-character YouTube video ID from the URL (or accepts a bare ID directly).
-
A VideoJS player is created inside the
#video_containerelement on the page, configured to use the YouTube tech with ads disabled. -
The YouTube IFrame API loads the video without ads and reports playback state back to VideoJS.
-
Once playback begins, the video is added (or updated) in your local playlist, and its title, author, and duration are captured from the YouTube metadata.
The diagram below shows the relationship between the main components:
Browser
┌───────────────────────────────────────────────────────┐
│ SkipAd module (skipad.js) │
│ │
│ ┌─────────────┐ videoLoad ┌─────────────────┐ │
│ │ inputWrapper│───────────────►│ embedRunVideo │ │
│ │ Handler │ │ (VideoJS + YT) │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ┌─────────────┐ add / update │ │
│ │ Playlist │◄────────────────────────┘ │
│ │ Manager │ │
│ │(localStorage│ render cards / list │
│ │ key: │──────────────────────────────────► │
│ │ playlist) │ DOM playlist block │
│ └─────────────┘ │
└───────────────────────────────────────────────────────┘Accepted URL Formats
You can paste any of the following into the video URL input field. SkipAd recognises all standard YouTube link styles as well as bare video IDs.
| Format | Example |
|---|---|
Standard watch URL | |
Short URL | |
Embed URL | |
Old | |
Bare 11-character video ID |
|
| If SkipAd cannot find a valid 11-character ID in your input it will silently ignore the request. Double-check that the link is a complete YouTube URL or that the ID is exactly 11 characters long. |
Configuration Options
SkipAd is configured through the J1 adapter system. The adapter passes an options object to the module at startup. The most important settings are described below.
videoJS.autoStart
Controls whether the VideoJS player is fully activated once the page loads. When set to false the module still initialises but will not attempt to embed any video until explicitly triggered.
| Type | Default |
|---|---|
|
|
videoJS.players.youtube.autoplay
Controls whether a video starts playing automatically as soon as it is loaded into the player. When false the video is loaded and paused, waiting for the user to press play.
| Type | Default |
|---|---|
|
|
videoJS.hideControlBar
When set to true, the VideoJS control bar is hidden and the native YouTube controls are suppressed, giving the video a clean, uncluttered look. The bar can be restored at runtime by calling vjsPlayer.removeClass('vjs-youtube-hide-controlbar') from the browser console.
| Type | Default |
|---|---|
|
|
videoJS.playbackRates
Configures the playback speed selector shown in the player control bar.
enabled-
When
true, a speed selector is added to the control bar. values-
An array of numbers that appear in the speed selector. Example:
[0.5, 0.75, 1, 1.25, 1.5, 2].
videoJS:
playbackRates:
enabled: true
values: [0.5, 0.75, 1, 1.25, 1.5, 2]videoJS.plugins.skipButtons
Adds backward and forward skip buttons to the player control bar so you can jump a fixed number of seconds without dragging the progress bar.
enabled-
Activates the skip-buttons plugin.
backward-
Number of seconds to skip backward (e.g.
10). forward-
Number of seconds to skip forward (e.g.
30). surroundPlayButton-
When
true, the backward button is placed immediately before and the forward button immediately after the main play/pause button.
videoJS:
plugins:
skipButtons:
enabled: true
backward: 10
forward: 30
surroundPlayButton: trueplaylist.loop
Enables sequential auto-play: when a video ends, SkipAd automatically loads the next video in your playlist according to the current sort order.
enabled-
Activates loop mode. A toggle switch also appears in the playlist header so the user can enable or disable looping at any time.
pip-
When
true, and when the browser supports the Document Picture-in-Picture API, a PiP button is added to the control bar. The video will also automatically enter PiP mode when the user switches to another tab while a video is playing.
playlist:
loop:
enabled: true
pip: trueModule API Reference
The public API of the skipAd module is the skipAd global object. The adapter exposes it after initialisation. The sections below describe every exported component.
skipAd.playlistManager
The playlistManager object is the central data store and rendering engine for the playlist. All data is kept in localStorage under the key playlist.
addEntry(entry)
Adds a new video to the top of the playlist, or — if the same video ID already exists — moves the existing entry to the top and refreshes its watch date. After saving, the visible playlist is re-rendered automatically.
| Parameter | Type | Required | Description |
|---|---|---|---|
entry | Object | yes | A playlist entry object. Required fields: |
Example:
skipAd.playlistManager.addEntry({
videoId: 'dQw4w9WgXcQ',
title: 'Never Gonna Give You Up',
watchDate: new Date().toISOString()
});deleteEntry(videoId)
Removes the entry with the given YouTube video ID from the playlist and re-renders the playlist UI.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID to remove. |
clearPlaylist()
Removes all entries from the playlist, invalidates the search index, and re-renders the (now empty) playlist UI. Returns true when entries were removed, false when the playlist was already empty.
| This operation cannot be undone unless you have previously exported the playlist to a JSON file. |
updateEntryDuration(videoId, durationSeconds)
Updates the stored duration for a playlist entry. This is called automatically once the player reports a valid duration via the durationchange event. You rarely need to call it manually.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
durationSeconds | Number | yes | Video duration in seconds (must be > 0). |
updateEntryAuthor(videoId, author)
Updates the channel name (author) for a playlist entry. Called automatically once the YouTube IFrame API supplies the author after playback begins.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
author | String | yes | Channel or author name. |
updateEntryPosition(videoId, positionSeconds)
Saves the current playback position so the video can be resumed from the same point next time. Called automatically on pause and end events.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
positionSeconds | Number | yes | Playback position in seconds (must be ≥ 0). |
updateWatchDate(videoId)
Refreshes the watch-date timestamp of a playlist entry to the current moment. This keeps the "time ago" display in the playlist accurate after a video is replayed.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
updateEntryRating(videoId, rating)
Saves a star rating (1–5) for a playlist entry. A rating of 0 clears an existing rating.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
rating | Number | yes | Rating value: 1–5, or 0 to clear. |
updateEntryFields(videoId, fields)
Updates one or more editable metadata fields for a playlist entry. Only the keys that are present in the fields object are changed; all other fields remain untouched.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
fields | Object | yes | An object containing any combination of: |
Example:
skipAd.playlistManager.updateEntryFields('dQw4w9WgXcQ', {
category: 'Music',
tags: 'pop, 80th, classic',
rating: 5
});getEntryPosition(videoId)
Returns the last saved playback position (in seconds) for the given video ID, or 0 if no position has been saved yet.
| Parameter | Type | Required | Description |
|---|---|---|---|
videoId | String | yes | The 11-character YouTube video ID. |
Returns: Number — saved position in seconds, or 0.
getNextVideoId(currentVideoId)
Returns the video ID of the next entry in the playlist after the given ID, according to the currently active sort order. Returns null when the current video is the last item or when the playlist has fewer than two entries. Used internally by loop mode.
| Parameter | Type | Required | Description |
|---|---|---|---|
currentVideoId | String | yes | The 11-character YouTube video ID that is currently playing. |
Returns: String|null — next video ID, or null if at the end of the list.
sortPlaylist(criterion)
Sorts the in-memory playlist and re-renders the UI using the given sort criterion. The active criterion is persisted so that subsequent renders use the same order.
| Parameter | Type | Required | Description |
|---|---|---|---|
criterion | String | yes | One of: |
searchPlaylist(query)
Runs a full-text search against the playlist using the Lunr.js index. The search covers the title, author, category, and tags fields. Returns the matching entries as an array and re-renders the playlist to show only those results.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | String | yes | Search expression (words, partial words, or phrases). |
Returns: Array — matching playlist entry objects.
Example:
const results = skipAd.playlistManager.searchPlaylist('concert live');
console.log(results.length + ' videos found');clearSearch()
Resets the active search filter and re-renders the full playlist.
buildSearchIndex()
Builds (or rebuilds) the Lunr.js full-text search index from all entries currently in localStorage. The index is then serialised and cached in localStorage under a separate key so it survives page reloads. Called automatically when a playlist is imported; you only need to call this manually if you modify localStorage data outside of the module.
importFromUrl(url)
Fetches a playlist JSON file from the given URL and replaces the current playlist (or merges into it when merge mode is active).
| Parameter | Type | Required | Description |
|---|---|---|---|
url | String | yes | Absolute or root-relative URL pointing to a JSON playlist file. |
importFromUrlAsync(url)
Async version of importFromUrl. Returns a Promise that resolves once the import is complete. Used internally by the server playlist import feature.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | String | yes | Absolute or root-relative URL pointing to a JSON playlist file. |
importFromFile()
Opens a native file-picker dialog filtered to .json files. After the user selects a file it is read, validated, and imported into localStorage. The playlist is then re-rendered automatically.
exportToFile([filename])
Downloads the current playlist as a JSON file. If no file name is given the file is named skipAd-playlist_yyyy-mm-dd__hh-mm-ss.json using the current date and time.
| Parameter | Type | Required | Description |
|---|---|---|---|
filename | String | no | Custom file name for the download (including |
Example:
// export with a custom name
skipAd.playlistManager.exportToFile('my-backup.json');
// export with the default timestamped name
skipAd.playlistManager.exportToFile();renderCurrent()
Re-renders the playlist UI using the currently active display mode (cards or list) and sort order. You rarely need to call this manually — all update*, delete*, import*, and clear* methods call it automatically.
renderCards()
Renders the playlist as a grid of visual cards, each showing the video thumbnail, title, author, watch date, duration, rating, category, and action buttons.
renderPlaylist()
Renders the playlist as a compact list where each row shows a small thumbnail, title, author, and time-ago information.
load()
Reads and returns the full playlist array from localStorage. Returns null when no playlist has been stored yet.
Returns: Array|null
save(playlist)
Serialises the given array to JSON and writes it to localStorage under the key playlist. This is the single point of persistence for all playlist data.
| Parameter | Type | Required | Description |
|---|---|---|---|
playlist | Array | yes | Array of playlist entry objects to persist. |
skipAd.playlistIOHandler
Wires up the Import, Export, Clear, and server-playlist Import buttons in the Your playlist section to the corresponding playlistManager methods. Instantiated automatically by the J1 adapter; you do not need to create it yourself.
skipAd.playlistSearchHandler
Connects the playlist search input field to playlistManager.searchPlaylist(). Typing in the field triggers a debounced search after 300 ms; pressing Enter triggers it immediately; pressing Escape clears the search. Instantiated automatically by the J1 adapter.
skipAd.playlistModeSwitchHandler
Manages the Cards toggle switch that appears above the playlist. When the switch is on (default) the playlist renders as cards; when it is off it renders as a list. The selected mode is persisted in localStorage so it survives page reloads.
skipAd.playlistMergeSwitchHandler
Manages the Merge toggle switch. When merge mode is active, importing a playlist (from a file or from the server) adds only videos that do not already exist in your current playlist — duplicates are silently skipped. When merge mode is off (default), importing replaces the current playlist entirely.
skipAd.playlistLoopSwitchHandler
Manages the Loop toggle switch that appears in the playlist header when loop mode is enabled in the configuration. The switch state is persisted in localStorage so the user’s preference survives page reloads.
skipAd.playlistSortHandler
Wires the sort <select> element in the playlist header to playlistManager.sortPlaylist(). Selecting a different sort criterion immediately re-sorts and re-renders the list.
Available sort criteria:
| Value | Description |
|---|---|
| Most recently watched first. |
| Oldest watched first. |
| Most recently published first. |
| Oldest published first. |
| Longest video first. |
| Shortest video first. |
| Alphabetical by title (A → Z). |
| Alphabetical by channel name (A → Z). |
| Alphabetical by category (A → Z). |
| Alphabetical by type (A → Z). |
| Ascending episode number within each series. |
skipAd.inputWrapperHandler
Manages the Video selection section at the top of the page. It handles:
-
The Paste button — reads the clipboard and populates the URL input.
-
Direct paste events (Ctrl+V) inside the input field.
-
The Watch Video button — extracts the video ID and starts playback.
-
The Clear (×) button — empties the URL input field.
-
The Enter key inside the URL input — equivalent to clicking Watch Video.
-
The Escape key inside the URL input — clears the field.
skipAd.inputValueBackgroundHandler
A visual helper that changes the background colour of every text input and select element on the page when the element has a value (filled state) versus when it is empty. Uses CSS custom properties --input-background and --card-background defined by the active J1 skin.
skipAd.navbarSmoothScrollHandler
Intercepts clicks on same-page anchor links inside the top navigation bar and delegates scrolling to j1.scrollToAnchor() for a smooth animated scroll instead of the browser’s default instant jump.
Playlist Entry Object
The following table describes every field that a playlist entry can contain. Fields marked auto are filled in automatically by the module and you do not normally need to supply them.
| Field | Type | Description |
|---|---|---|
| String | (Required) The 11-character YouTube video ID. |
| String | (Required) Video title as reported by the YouTube API. |
| String | (Required / auto) ISO 8601 timestamp of the last watch event. |
| String | (auto) YouTube channel name. Populated once playback starts. |
| Number | (auto) Video length in seconds. Populated from the |
| Number | (auto) Last saved playback position in seconds. |
| Number | User star rating: 1–5, or |
| String | User-defined category (e.g. |
| String | Comma-separated tag list used by the search index. |
| Number | Series number for episode-based playlists ( |
| Number | Episode number within a series ( |
| String | Content type (e.g. |
| String | Original publication date in ISO |
| String | Optional URL to an external information page (must be HTTP or HTTPS). |
| String | Optional URL to an alternative video source (must be HTTP or HTTPS). |
| String | Optional free-text description of the video. |
Playlist JSON File Format
When you export your playlist the resulting JSON file uses the following structure. The same format is required when importing a file.
{
"meta_data": {
"exported_at": "2026-04-18T10:23:00.000Z",
"count": 2
},
"playlist": [
{
"videoId": "dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"author": "Rick Astley",
"watchDate": "2026-04-18T09:00:00.000Z",
"duration": 213,
"lastPosition": 0,
"rating": 5,
"category": "Music",
"tags": "pop, 80th, classic",
"series": 0,
"episode": 0,
"type": "videoclip",
"issueDate": "1987-07-27",
"infoLink": "",
"videoLink": "",
"description": ""
}
]
}| Older exports may contain a plain JSON array (no |
Custom DOM Events
SkipAd fires and listens to several custom events on the document object. You can attach your own listeners to these events to extend the module’s behaviour.
| Event name | Description |
|---|---|
| Fired by the input handler when a valid video ID has been extracted from the URL input and playback is about to start. The event |
| Fired once when the VideoJS player transitions from |
| Fired when the YouTube IFrame API returns the video title and author after the player is ready. The event |
Example — listen for a new video load:
document.addEventListener('videoLoad', (e) => {
console.log('Loading video: ' + e.detail.videoId);
});Player State Mapping
SkipAd translates YouTube IFrame API player states into standard VideoJS event names so that plugins and custom code can listen to familiar events regardless of the underlying playback technology.
| YouTube state code | VideoJS event | Meaning |
|---|---|---|
|
| Video not started yet (unstarted). |
|
| Playback has reached the end. |
|
| Video is currently playing. |
|
| Video has been paused. |
|
| Player is buffering. |
Built-in Tag Genres
When editing a playlist entry, the tag selector groups common tags by genre to make it easy to categorise your videos. The following genres and their associated tags are built into the module.
| Genre | Tags |
|---|---|
Beauty | beauty, makeup, skincare, hairstyle, fashion |
Comedy | comedy, standup, memes |
Education | education, learn, tutorial, study |
Entertainment | entertainment, infotainment, edutainment, dance, show, musical, tv, mixed, cinema, podcast, magician, horror, ventriloquist, celebrity, series, competition, audioclip, videoclip |
Music | music, concert, festival, cover, pop, k-pop, classic, rock, folk, traditional, latin, jazz, rap, singer, songwriter, original, emotional, romantic, 60th, 70th, 80th, 90th, remix, live |
Gaming | game, gamer |
Howto | howto, diy, tech, mounting, cooking, food |
Product | product, reviews, tech, unboxing, ai, programming, gadgets |
News | news, commentary, viral, trending |
Fitness | fitness, health, sport, workout, gym, motivation |
Business | business, finance, tech, ai, programming, gadgets |
Logging and Development Mode
SkipAd uses the log4javascript library for structured logging. Log output is controlled by the env field of the adapter options.
developmentordev-
Full debug, info, and warn messages are written to the browser console, including a unique session ID and a precise timestamp for each message.
production(default)-
Only
ERROR-level messages are written to the console. All debug and info output is suppressed.
You can temporarily switch to development mode in your J1 adapter configuration to trace unexpected behaviour:
adapter:
skipad:
env: development| Never set |