Plugin Developer Guide¶
Build integrations for media players like Plex, Jellyfin, Kodi, and more.
Overview¶
A typical OpenSkip integration:
- Detects when media starts playing
- Identifies the show, season, and episode
- Queries OpenSkip for timestamps
- Displays skip buttons or auto-skips
Getting Show Metadata¶
Most media players provide metadata about what's playing. Look for:
| Field | Use |
|---|---|
| TVDB ID | Best option - query directly with tvdb_id |
| TMDB ID | Query shows endpoint with tmdb_id |
| IMDB ID | Query shows endpoint |
| Show name + Season + Episode | Search by title, then get episode |
Example: Plex¶
Plex provides GUID strings that often contain TVDB IDs:
# Plex GUID format: com.plexapp.agents.thetvdb://81189/1/1
def parse_plex_guid(guid: str):
if "thetvdb" in guid:
parts = guid.split("//")[1].split("/")
return {
"tvdb_id": int(parts[0]),
"season": int(parts[1]),
"episode": int(parts[2])
}
return None
Example: Jellyfin¶
Jellyfin stores provider IDs in the item metadata:
def get_jellyfin_ids(item):
provider_ids = item.get("ProviderIds", {})
return {
"tvdb_id": provider_ids.get("Tvdb"),
"tmdb_id": provider_ids.get("Tmdb"),
"imdb_id": provider_ids.get("Imdb")
}
Querying OpenSkip¶
Primary Method: TVDB ID¶
import requests
def get_timestamps(tvdb_id: int, season: int, episode: int, duration_ms: int = None):
params = {
"tvdb_id": tvdb_id,
"season": season,
"episode": episode
}
if duration_ms:
params["duration_ms"] = duration_ms
params["tolerance"] = 5000 # 5 second tolerance
response = requests.get(
"https://api.openskip.io/api/v1/timestamps",
params=params,
timeout=5
)
if response.status_code == 200:
data = response.json()
return data["items"][0] if data["total"] > 0 else None
return None
Fallback: Search by Title¶
If you don't have a TVDB ID:
def find_show_by_title(title: str):
response = requests.get(
"https://api.openskip.io/api/v1/shows",
params={"search": title}
)
data = response.json()
if data["total"] > 0:
return data["items"][0]
return None
def get_timestamps_by_title(title: str, season: int, episode: int):
show = find_show_by_title(title)
if not show:
return None
# Get episode
response = requests.get(
f"https://api.openskip.io/api/v1/shows/{show['id']}/seasons/{season}/episodes/{episode}"
)
if response.status_code != 200:
return None
ep = response.json()
# Get timestamps
return get_timestamps_by_episode_id(ep["id"])
Implementing Skip Functionality¶
Skip Button Approach¶
Show a button when entering skippable region:
class SkipButtonController:
def __init__(self, timestamps):
self.timestamps = timestamps
self.button_visible = False
def on_playback_position(self, current_time: float):
# Check if in intro region
intro_start = self.timestamps.get("intro_start")
intro_end = self.timestamps.get("intro_end")
if intro_start and intro_end:
if intro_start <= current_time < intro_end:
if not self.button_visible:
self.show_skip_button("Skip Intro", intro_end)
self.button_visible = True
else:
if self.button_visible:
self.hide_skip_button()
self.button_visible = False
# Similarly for outro, recap, etc.
Auto-Skip Approach¶
Automatically skip without user interaction:
class AutoSkipController:
def __init__(self, timestamps, player):
self.timestamps = timestamps
self.player = player
self.skipped_intro = False
self.skipped_outro = False
def on_playback_position(self, current_time: float):
intro_start = self.timestamps.get("intro_start")
intro_end = self.timestamps.get("intro_end")
# Auto-skip intro
if intro_start and intro_end and not self.skipped_intro:
if intro_start <= current_time < intro_end:
self.player.seek(intro_end)
self.skipped_intro = True
self.notify("Skipped intro")
Best Practices¶
Caching¶
Cache timestamp data to reduce API calls and improve responsiveness:
from functools import lru_cache
from datetime import datetime, timedelta
class TimestampCache:
def __init__(self, ttl_hours=24):
self.cache = {}
self.ttl = timedelta(hours=ttl_hours)
def get(self, key):
if key in self.cache:
data, timestamp = self.cache[key]
if datetime.now() - timestamp < self.ttl:
return data
del self.cache[key]
return None
def set(self, key, data):
self.cache[key] = (data, datetime.now())
Error Handling¶
Handle API failures gracefully:
def get_timestamps_safe(tvdb_id, season, episode):
try:
response = requests.get(
"https://api.openskip.io/api/v1/timestamps",
params={"tvdb_id": tvdb_id, "season": season, "episode": episode},
timeout=5
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
logger.warning("OpenSkip API timeout")
return None
except requests.exceptions.RequestException as e:
logger.error(f"OpenSkip API error: {e}")
return None
Rate Limiting¶
Respect API rate limits (60 requests/minute for search):
import time
from threading import Lock
class RateLimiter:
def __init__(self, requests_per_minute=50):
self.min_interval = 60 / requests_per_minute
self.last_request = 0
self.lock = Lock()
def wait(self):
with self.lock:
now = time.time()
elapsed = now - self.last_request
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
self.last_request = time.time()
Configuration Options¶
Let users configure skip behavior:
class SkipConfig:
# Skip behavior
auto_skip_intro: bool = False
auto_skip_outro: bool = False
auto_skip_recap: bool = False
show_skip_button: bool = True
skip_button_duration: int = 10 # seconds to show button
# API settings
api_url: str = "https://api.openskip.io"
cache_ttl_hours: int = 24
timeout_seconds: int = 5
Testing Your Integration¶
Test Shows¶
Use these TVDB IDs for testing (when data is available):
| Show | TVDB ID |
|---|---|
| Breaking Bad | 81189 |
| Game of Thrones | 121361 |
| The Office (US) | 73244 |
Mock API for Development¶
class MockOpenSkipAPI:
def get_timestamps(self, tvdb_id, season, episode):
return {
"items": [{
"intro_start": 0.0,
"intro_end": 45.0,
"outro_start": 3400.0,
"outro_end": 3480.0,
}],
"total": 1
}
Publishing Your Plugin¶
When your plugin is ready:
- Add OpenSkip attribution/link
- Document the OpenSkip integration
- Let us know! We'd love to list community plugins
Need Help?¶
- Check the API Reference
- Open an issue on GitHub
- Join our community on Discord