- Rust 70.7%
- C# 14.6%
- Svelte 9.2%
- TypeScript 3.9%
- Nix 0.9%
- Other 0.6%
Jellyfin plugin: - Use ResolvePaths (plural) with explicit collectionType from GetContentType() so SeriesResolver and MovieResolver work correctly - Auto-detect and re-create items stored as plain Video or Folder - Skip plain Folder items in WalkUpToAncestor, stop at library roots Emby plugin: - Sync stale item cleanup on file upgrades (Removed status) |
||
|---|---|---|
| autopulse | ||
| emby-plugin | ||
| jellyfin-plugin | ||
| .gitignore | ||
| jellyfin-targeted-scan-v1.3.0.zip | ||
| jellyfin-targeted-scan-v1.9.0.zip | ||
| manifest.json | ||
| README.md | ||
Targeted Scans
Jellyfin & Emby plugins for instant, targeted library scanning — no full library scans needed.
Includes a modified Autopulse fork that connects Sonarr/Radarr to the plugins automatically.
The Problem
When new media arrives on disk, Jellyfin and Emby require a full library scan to discover it. On large libraries (1000+ shows), this can take minutes to hours.
The Solution
Targeted Scans adds a POST /Library/ScanPath API endpoint to Jellyfin and Emby that instantly creates the new item in the database and queues a metadata refresh — under a second regardless of library size.
The included Autopulse fork bridges Sonarr/Radarr to the plugin automatically:
Sonarr/Radarr → (webhook) → Autopulse → (ScanPath API) → Jellyfin/Emby Plugin → Item Created
How the plugin works
- Receives a filesystem path (e.g.
/media/TV/Breaking Bad/Season 1/S01E01.mkv) - Walks up the directory tree to find the nearest known ancestor in the library database
- Creates all intermediate items (Series, Season, Episode) from the top down
- Queues metadata refresh for each created item
- Returns the item ID and status
Full Setup Guide
Step 1: Install the Plugin
Jellyfin (10.11+)
Option A: Plugin Repository (Recommended)
- Go to Dashboard > Plugins > Repositories
- Add a new repository:
- Name:
Targeted Scans - URL:
https://raw.githubusercontent.com/d3v1l1989/targeted-scans/main/manifest.json
- Name:
- Go to Catalog and search for TargetedScan
- Install and restart Jellyfin
Option B: Manual Install
- Download
jellyfin-targeted-scan-vX.X.X.zipfrom Releases - Extract to your Jellyfin plugins directory:
{JellyfinDataPath}/plugins/TargetedScan/ - Restart Jellyfin
Emby
- Download
EmbyTargetedScan.dllfrom Releases - Copy directly to your Emby plugins directory (not in a subfolder):
{EmbyConfigPath}/plugins/EmbyTargetedScan.dll - Restart Emby
Step 2: Deploy Autopulse
The Autopulse fork receives webhooks from Sonarr/Radarr and calls the ScanPath endpoint on your media server. A pre-built Docker image is available on Docker Hub.
docker-compose.yml
services:
autopulse:
restart: unless-stopped
container_name: autopulse
image: d3v1l1989/autopulse-targeted:latest
hostname: autopulse
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 -O /dev/null http://127.0.0.1:2875/stats || exit 1"]
interval: 10s
timeout: 5s
retries: 3
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- AUTOPULSE__APP__DATABASE_URL=sqlite://data/autopulse.db
volumes:
- ./data:/app # config.yaml goes here
- /mnt:/mnt:rslave # mount your media paths (must match Sonarr/Radarr paths)
- /etc/localtime:/etc/localtime:ro
ports:
- "2875:2875"
autopulse-ui:
restart: unless-stopped
container_name: autopulse-ui
image: danonline/autopulse:ui
hostname: autopulse-ui
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- FORCE_SERVER_URL=true
- DEFAULT_SERVER_URL=http://autopulse:2875
- ORIGIN=http://localhost:2880 # change to your domain if using reverse proxy
ports:
- "2880:2880"
data/config.yaml
app:
log_level: debug
database_url: sqlite:///app/autopulse.db
auth:
username: your_username
password: your_password
opts:
check_path: true
max_retries: 5
default_timer_wait: 10
triggers:
sonarr:
type: sonarr
rewrite:
from: /mnt/media
to: /mnt/media
radarr:
type: radarr
rewrite:
from: /mnt/media
to: /mnt/media
lidarr:
type: lidarr
rewrite:
from: /mnt/media
to: /mnt/media
targets:
plex:
type: plex
url: http://plex:32400
token: YOUR_PLEX_TOKEN
refresh: true
emby:
type: emby
url: http://emby:8096
token: YOUR_EMBY_API_KEY
refresh_metadata: true
jellyfin:
type: jellyfin
url: http://jellyfin:8096
token: YOUR_JELLYFIN_API_KEY
refresh_metadata: true
Path rewriting: If Sonarr sees files at
/downloads/tv/...but Jellyfin sees them at/media/tv/..., setfrom: /downloads/tvandto: /media/tv. If they share the same mount paths, set both to the same value.
Each trigger becomes a webhook endpoint at http://autopulse:2875/triggers/<name> — you'll use these URLs in the next step.
mkdir -p data
# create your config.yaml in data/
docker compose up -d
This starts two containers:
- autopulse on port
2875— the API that receives webhooks and triggers scans - autopulse-ui on port
2880— a web dashboard to monitor scan events
Step 3: Configure Sonarr/Radarr Webhooks
This connects your *arr apps to Autopulse. When Sonarr/Radarr downloads or renames media, it sends a webhook to Autopulse, which triggers the plugin.
Sonarr
- Go to Settings > Connect > + > Webhook
- Fill in:
- Name:
Autopulse - URL:
http://autopulse:2875/triggers/sonarr - Method:
POST - Username: your autopulse
auth.username - Password: your autopulse
auth.password
- Name:
- Select events:
- On File Import
- On File Upgrade
- On Import Complete
- On Episode File Delete
- On Episode File Delete For Upgrade
- Click Test then Save
Radarr
- Go to Settings > Connect > + > Webhook
- Fill in:
- Name:
Autopulse - URL:
http://autopulse:2875/triggers/radarr - Method:
POST - Username: your autopulse
auth.username - Password: your autopulse
auth.password
- Name:
- Select events:
- On File Import
- On File Upgrade
- On Movie File Delete
- On Movie File Delete For Upgrade
- Click Test then Save
Multiple Instances
If you run multiple Sonarr/Radarr instances (e.g. for different languages or qualities), create a separate trigger for each:
triggers:
sonarr:
type: sonarr
rewrite:
from: /mnt/media
to: /mnt/media
sonarranime:
type: sonarr
rewrite:
from: /mnt/media
to: /mnt/media
radarr:
type: radarr
rewrite:
from: /mnt/media
to: /mnt/media
radarranime:
type: radarr
rewrite:
from: /mnt/media
to: /mnt/media
Then point each instance's webhook URL to its matching trigger name (e.g. http://autopulse:2875/triggers/sonarranime).
Step 4: Verify the Full Pipeline
- Grab something in Sonarr — trigger a manual search for an episode
- Watch Autopulse logs:
docker logs -f autopulse - You should see:
- Webhook received from Sonarr
- Path extracted and rewritten
ScanPathcalled on Jellyfin/Emby- Item created or refreshed
- Check Jellyfin/Emby — the episode should appear immediately with metadata
API Reference
Both plugins expose identical endpoints. You only need these if you're calling the plugin directly (Autopulse handles this automatically).
POST /Library/ScanPath
Scan a single path.
Headers:
Content-Type: application/json
Authorization: MediaBrowser Token="YOUR_API_KEY"
Request:
{
"Path": "/media/TV/Breaking Bad/Season 1/S01E01.mkv"
}
Response:
{
"ItemId": "abc123...",
"ItemName": "S01E01",
"Status": "Created",
"Message": "Item created and metadata refresh queued"
}
POST /Library/ScanPaths
Scan multiple paths in a single batch.
Request:
{
"Paths": [
"/media/TV/Breaking Bad/Season 1/S01E01.mkv",
"/media/TV/Breaking Bad/Season 1/S01E02.mkv"
]
}
Response:
{
"Results": [
{
"ItemId": "abc123...",
"ItemName": "S01E01",
"Status": "Created",
"Message": "/media/TV/Breaking Bad/Season 1/S01E01.mkv"
}
]
}
Status Values
| Status | Description |
|---|---|
Created |
New item created and metadata refresh queued |
Refreshed |
Item already existed, metadata refresh queued |
PathNotFound |
Path does not exist on the filesystem |
ParentNotFound |
Could not find a parent library item for the path |
Failed |
Scan failed |
License
Jellyfin and Emby plugins are provided as-is. The autopulse fork is licensed under the same terms as the original project.