-
Notifications
You must be signed in to change notification settings - Fork 25
Site integrations
This extension injects a small Servarr search icon into third-party sites (IMDb, TMDb, Trakt, etc.). Clicking the icon sends a search term (optionally prefixed with an ID like imdb: or tmdb:) to the user’s chosen Servarr app (Sonarr, Radarr, Lidarr, Readarr).
We now use “integration engines” instead of a single integrations array inside content_script.js.
- Each site lives in its own file under:
src/content/engines/integrations/<site>.js - A small runtime (
index.js+default.js) registers engines in
window.__servarrEngines.list. -
content_script.jsiterates registered engines and injects icons.
There’s still a simple registry in core.js (id, name, logo, enabled flag) that controls visibility/toggles in the settings UI.
Create a file src/content/engines/integrations/yoursite.js:
// src/content/engines/integrations/yoursite.js
(function () {
if (!window.__servarrEngines) window.__servarrEngines = { list: [], helpers: {} };
const Def = window.__servarrEngines.helpers.DefaultEngine;
const pick = window.__servarrEngines.helpers.pickSiteIdFromDocument; // optional helper
// Build an engine using the DefaultEngine config
const YourSiteEngine = Def({
id: 'yoursite',
// Where to run (simple substring checks against window.location.href)
// Not required if using a custom `match` function below.
urlIncludes: ['yoursite.example.com/path'],
// When to run (more complex logic, e.g., regex + DOM gates)
// Not required if using `urlIncludes` above.
match: function (document, url) {
// Example: simple regex match on the URL
urlMatches = /.+letterboxd\.com\/film\/.+/i.test(url);
if (!urlMatches) return false;
// Example: gate on DOM content
return !(document.querySelector('a[href*="themoviedb.org/movie/"]'));
},
// Where to place the icon (container to inject into)
containerSelector: 'h1.title',
// Prepend/append/before/after within the container
insertWhere: 'prepend', // 'prepend' | 'append' | 'before' | 'after'
// Optional wrapper around the <a> (for tricky layouts)
// wrapLinkWithContainer: '<div class="slot"></div>',
// Optional: wait for SPA content to render (ms)
// deferMs: 1000,
// Decide which Servarr app to target:
// 1) Fixed (use this)
// siteType: 'sonarr', // 'sonarr' | 'radarr' | 'lidarr' | 'readarr_ebook' | 'readarr_audiobook'
// 2) OR dynamic (use this): pick based on DOM/content
resolveSiteType: function (document, url, settings) {
// Example of rule-based routing (like the old `rules`):
// Read a value from the page (e.g., og:type), then match patterns.
// Return 'sonarr' or 'radarr' (etc.), or null to skip.
return pick(document, 'meta[property=\"og:type\"]', 'content', [
{ siteId: 'sonarr', pattern: /video\.tv_show/i },
{ siteId: 'radarr', pattern: /video\.movie/i },
]);
},
// How to extract the search term for the link
getSearch: function (el, document) {
// Example 1: plain text from an element
// return (document.querySelector('h1.title')?.textContent || '').trim();
// Example 2: pull an ID from a URL & prefix it
// const href = document.querySelector('link[rel=\"canonical\"]')?.href || '';
// const m = href.match(/\/(?<id>\d{2,10})-/);
// return m ? 'tmdb:' + m.groups.id : '';
return '';
},
// Icon styles (you can stick to the defaults or tweak per site)
iconStyle: 'width: 28px; margin: -4px 10px 0 0;',
// Optional: SPA (Single Page Application) support for sites with client-side routing
// Enable this if the site changes URLs without full page reloads (like Trakt, SIMKL, etc.)
spa: {
domains: ['yoursite.example.com'], // Domain substrings to monitor for URL changes
urlCheckIntervalMs: 500 // How often to check for URL changes (optional, defaults to 500ms)
}
});
window.__servarrEngines.list.push(YourSiteEngine);
})();-
DefaultEngine(config)– builds the engine. -
pickSiteIdFromDocument(document, selector, attribute, rules)– replicates oldruleslogic:rules = [{ siteId:'sonarr', pattern:/.../i }, ...]- Reads the
attributefrom the firstselectormatch and returns the first matchingsiteId.
-
createNodeFromHTML(html)– safely wraps the link in custom HTML (for layout-specific needs).
| Old (integrations array) | New (engine config) |
|---|---|
match.terms |
urlIncludes (array of substrings) |
defaultSite |
siteType (fixed) |
rules (with match.pattern / operator) |
resolveSiteType(document, url, settings) + pickSiteIdFromDocument helper |
search.containerSelector, selectorType, modifiers
|
Implement inside getSearch(el, document) (regex, replace, prepend, etc.) |
icon.containerSelector |
containerSelector |
icon.locator (prepend, append) |
insertWhere (prepend, append, before, after) |
icon.wrapLinkWithContainer |
wrapLinkWithContainer |
icon.imgStyles |
iconStyle |
deferMs |
deferMs (unchanged) |
where gates |
Implement inside resolveSiteType (return null to skip) or early-exit in getSearch
|
| (new) |
spa (SPA/client-side routing support) |
You can keep the same behaviours: extract IDs (
imdb:tt…,tmdb:…), trim/rewrite titles, or gate onog:type, etc.
Make sure these are loaded (in this order) as content scripts:
-
content/engines/index.js(initializes the registry) -
content/engines/default.js(runtime + helpers) - Your engine file(s) – e.g.,
content/engines/integrations/yoursite.js -
content/js/content_script.js(runner that executes engines)
Many engines ship with the repo already (IMDb, TMDb, TVDB, Trakt, etc.). Add yours after those.
Add an entry to core.js (the list that powers the toggles & logos):
// Somewhere in defaultSettings.integrations (or the relevant registry)
{
id: 'yoursite',
name: 'Your Site',
image: 'yoursite.png',
enabled: true
}The id must match your engine’s id.
IMDb (TV vs Movie via og:type):
resolveSiteType: function (document) {
return pick(document, 'meta[property=\"og:type\"]', 'content', [
{ siteId: 'sonarr', pattern: /(tv_show|other)/i },
{ siteId: 'radarr', pattern: /(movie|other)/i }
]);
},
getSearch: function (_el, document) {
const href = document.querySelector('link[rel=\"canonical\"]')?.href || '';
const m = href.match(/(?<tt>tt\d{5,10})/i);
return m ? 'imdb:' + m.groups.tt : '';
}TMDb (TV = Sonarr by title text, Movie = Radarr by canonical ID):
resolveSiteType: function (document, url) {
if (/themoviedb\.org\/tv\//i.test(url)) return 'sonarr';
if (/themoviedb\.org\/movie\//i.test(url)) return 'radarr';
return null;
},
getSearch: function (_el, document) {
const href = document.querySelector('link[rel=\"canonical\"]')?.href || '';
const isMovie = /themoviedb\.org\/movie\//i.test(href);
if (isMovie) {
const m = href.match(/\/(\d{2,10})-/);
return m ? 'tmdb:' + m[1] : '';
}
return (document.querySelector('.header .title h2 a')?.textContent || '').trim();
}Some modern websites use client-side routing where the URL changes without full page reloads (examples: Trakt.tv, Netflix, many React/Vue apps). The extension needs special handling for these sites since the content script doesn't automatically re-run when the URL changes.
Add the spa configuration to your engine:
const YourSpaEngine = Def({
id: 'yoursite',
urlIncludes: ['yoursite.com/content/'],
// Required: SPA configuration
spa: {
domains: ['yoursite.com'], // Domains to monitor for URL changes
urlCheckIntervalMs: 500 // Optional: check frequency (default: 500ms)
},
// ... rest of your engine config
});-
Domain Matching: The extension checks if the current URL contains any domain from the
spa.domainsarray -
URL Monitoring: If on a SPA domain, it starts monitoring for URL changes every
urlCheckIntervalMs -
Automatic Re-injection: When the URL changes within the SPA domain:
- Cleans up existing icons and markers
- Re-runs all engines for the new URL
- Injects icons if the new URL matches engine criteria
- Smart Cleanup: Stops monitoring when navigating away from SPA domains entirely
| Property | Type | Required | Description |
|---|---|---|---|
domains |
string[] |
Yes | Array of domain substrings to monitor (e.g., ['trakt.tv', 'netflix.com']) |
urlCheckIntervalMs |
number |
No | URL change check frequency in milliseconds (default: 500) |
Trakt.tv (real implementation):
spa: {
domains: ['trakt.tv'],
urlCheckIntervalMs: 500
}Multiple domains:
spa: {
domains: ['netflix.com', 'netflix.ca', 'netflix.co.uk'],
urlCheckIntervalMs: 750
}-
Opt-in Feature: Only engines with
spaconfiguration get URL monitoring -
Performance: Lower
urlCheckIntervalMs= more responsive but higher CPU usage -
Domain Specificity: Use specific domains to avoid false positives (prefer
'app.trakt.tv'over'trakt') - Multiple Engines: If multiple engines have SPA config for the same domain, the first enabled one wins
- User Settings: SPA monitoring only works for engines enabled in user settings
If the target DOM is built late (Trakt, SIMKL, Prime Video), add:
deferMs: 1000 // or 2000/3000 based on observationFor SPA sites that change URLs without page reloads, use the spa configuration instead of or in addition to deferMs. See the SPA Support section above for details.
- Prefer inline placement (
prependorappend) when possible for stable flow. - If a container can’t hold anchors or needs a stable slot, use
wrapLinkWithContainer:wrapLinkWithContainer: '<div class="my-slot"></div>' - Avoid negative margins unless the site’s markup forces it.
- Enable Debug in extension settings to see logs from
content_script.js. - Confirm:
- Engine
matchtriggers on the URL. -
resolveSiteTypereturns a Servarr type ornull(skip). -
getSearchreturns a non-empty term ("tmdb:12345","imdb:tt1234567", or a clear title). - The icon appears only once per target element (the runner prevents double injection).
- Engine
- For SPA pages, experiment with
deferMs. -
For SPA sites:
- Check console logs for "Starting URL change detection for SPA domain" when visiting the domain
- Verify URL changes are detected with "URL changed from ... to ... - re-running engines" logs
- Test navigation within the SPA to ensure icons appear on new pages
- Confirm monitoring stops when leaving the SPA domain entirely
- New engine file at
src/content/engines/integrations/<site>.js - Added to load order (manifest or build config) before
content_script.js - Settings entry in
core.js(id,name,image,enabled) - Tested on representative URLs
- Handled dynamic routing (
resolveSiteType) if required - Used
wrapLinkWithContainerfor tricky layouts if needed - No duplicate injection (verify
data-servarr-iconis respected) - Added logo asset (if the settings UI lists logos)
Q: I need to check multiple DOM gates like the old where.
Use resolveSiteType (return null to skip) or early return '' inside getSearch.
Q: I previously used modifiers (replace, regex-match, prepend).
Do those inside getSearch:
-
replace:
text = text.replace(/from/i, 'to') -
regex-match:
const m = str.match(/(?<id>tt\d+)/); return m ? 'imdb:'+m.groups.id : '' - prepend: Just concatenate the prefix string.
Q: Can I still support the “Custom icon position” floating CTA?
Yes—content_script.js decides that at runtime. Engines only provide terms, locations, and site type; the runner handles custom placement when enabled.