# Kodi Addon Architecture Reference

Audit results for three major Kodi video addons (Fen Light+, Seren, Umbrella) — their scraper pipelines, debrid integration, caching, and catalog backends. Useful as reference when designing Stremio scraper pipelines or debrid-based streaming systems.

## Addon Overview

| Addon | Version | Vendor | Entry Point | Lines | Size |
|-------|---------|--------|-------------|-------|------|
| **Fen Light+** | 2.3.2 | thejason40 (fork of Tikipeter) | `fenlight.py` → `router.py` | ~25K | 2.5MB |
| **Seren** | 3.0.1 | Nixgates | `seren.py` → `router.py` | ~20K | 20MB |
| **Umbrella** | 6.7.79 | umbrellaplug | `umbrella.py` → `router.py` | ~35K | 886MB (repo) |

All three are Kodi video addons (Python 3, GPL-3.0). **NOT Stremio compatible.** They are debrid-client apps designed to stream from cloud storage.

## Common Architecture Pattern

```
User Input → Router → Metadata Indexers → Scrapers → Debrid Cache Check → Source Resolution → Player
```

## Scraper Interface Patterns

This is critical when building combined addons. There are TWO distinct interfaces across these codebases:

### Interface A — Fen Light+ style (`results(self, info)`)

Used by Fen Light+ scrapers: `rd_cloud`, `pm_cloud`, `ad_cloud`, `tb_cloud`, `oc_cloud`, `easynews`, `folders`, `external`

```python
class source:
    def __init__(self):
        self.scrape_provider = 'rd_cloud'

    def results(self, info):
        # info dict: media_type, tmdb_id, title, year, season, episode,
        #            imdb_id, tvdb_id, aliases, expiry_times
        ...
        # Must call: internal_results(self.scrape_provider, self.sources)
        # which writes to window property for collection
        return self.sources  # list of source dicts
```

Source dict fields: `name`, `display_name`, `quality`, `size`, `size_label`, `extraInfo`, `url_dl`, `id`, `downloads`, `direct`, `source`, `scrape_provider`, `url`

### Interface B — Umbrella style (`sources(self, data, hostDict)`)

Used by Umbrella scrapers: `rd_cloud`, `pm_cloud`, `ad_cloud`, `tb_cloud`, `oc_cloud`, `easynews`, `filepursuit`, `gdrive`, `plexshare`

```python
class source:
    pack_capable = False   # class attribute
    hasMovies = True       # class attribute
    hasEpisodes = True     # class attribute

    def __init__(self):
        self.language = ['en']

    def sources(self, data, hostDict):
        # data dict: title, year, imdb, tvdb, season, episode,
        #            tvshowtitle, aliases, debrid_service, debrid_token, premiered
        # hostDict: list for hoster support (usually empty [])
        ...
        return sources  # list of source dicts
```

Source dict fields: `name`, `url`, `hash`, `quality`, `size`, `info`, `debrid`, `provider`, `seeders`, `pack`, `language`

### Adapter Pattern (Interface B → Interface A)

When combining scrapers from both families, wrap Interface B scrapers in an adapter:

```python
def create_adapter(name, source_class):
    class AdaptedSource:
        def __init__(self):
            self.scrape_provider = name
            self._inner = source_class()

        def results(self, info):
            # Build data dict from info
            data = {
                'title': info.get('title'), 'year': info.get('year'),
                'imdb': info.get('imdb_id'), 'season': str(info.get('season', '')),
                'episode': str(info.get('episode', '')), ...
            }
            raw = self._inner.sources(data, [])
            # Normalize to Interface A field names
            return [{'name': s.get('name'), 'url_dl': s.get('url'),
                     'quality': s.get('quality'), 'size': s.get('size', 0),
                     'scrape_provider': self.scrape_provider, ...} for s in raw]
    return AdaptedSource
```

## Metadata / Catalog Backends

All three use TMDB for catalog metadata + Trakt for user lists/progress:

| Feature | Fen Light+ | Seren | Umbrella |
|---------|-----------|-------|----------|
| Primary metadata | TMDB API v3 | TMDB API v3 | TMDB API v3 |
| Secondary | IMDB, OMDb, TVDB | OMDb, TVDB, Fanart.tv | TVMaze, Fanart.tv |
| List provider | Trakt API | Trakt API | Trakt, SimKL, MDBList |
| Watchlist/Progress | Trakt | Trakt (full DB sync) | Trakt |
| Artwork | TMDB images | Fanart.tv | Fanart.tv |

## Scraper Pipeline Comparison

### Fen Light+ (most modular, best base for combined addons)

```
Sources().playback_prep()
  → determine_scrapers_status()
    → active_internal_scrapers: based on user toggles for each scraper
    → active_external: if 'external' toggle is on + debrid enabled
  → get_sources()
    → prescrape check (internal scrapers only)
    → collect_results()
      → internal scrapers: rd_cloud, pm_cloud, ad_cloud, oc_cloud, tb_cloud, easynews
        (loaded via manual_function_import('scrapers.%s' % name, 'source'))
      → external scrapers: loaded from separate Kodi addon packages
        (imported via append_module_to_syspath + manual_module_import)
      → folder scrapers: local filesystem paths
    → process_results()
      → sort by quality/provider/size
      → filter by quality/size/audio/codec/HDR
      → debrid cache check: hash → check RD/PM/AD/OC/TB → mark cached/uncached
  → play_source() → display_results() or play_file()
```

**Key loading mechanism** (`internal_sources()`):
```python
def internal_sources(self, prescrape=False):
    active_sources = [i for i in self.active_internal_scrapers if i in internal_include_list]
    sourceDict = [('internal', manual_function_import('scrapers.%s' % i, 'source'), i)
                  for i in active_sources]
    return sourceDict
```

Each scraper module at `scrapers/{name}.py` must export a `source` class.

### Seren (most structured, full provider management)

```
Sources.__init__(item_information)
  → ThreadPool for each provider type (torrent/hoster/adaptive/direct)
  → Loads from `providerModules/` directory (zip packages)
  → Cloud scraper base class (CloudScraper) with regex matching
  → ProviderInstallManager with download → verify → extract → register flow
```

### Umbrella (most internal scrapers)

```
Sources().play()
  → internal scrapers: easynews, filepursuit, gdrive, plexshare
    (loaded via walk_packages in internal_scrapers/__init__.py)
  → cloud scrapers: rd_cloud, pm_cloud, ad_cloud, tb_cloud, oc_cloud
    (each queries debrid API for user's cloud files)
  → external providers (same pattern as Fen — separate Kodi addon)
  → Pre-resolved source cache between episodes
```

## Combined Addon Builds (June 2026)

Two unified addons were built at `~/addon-audit/build/` by combining Fen Light+'s architecture with Umbrella's additional scrapers:

### Ultimate Debrid — `~/addon-audit/build/ultimate-debrid/`

**93 files, 1.4MB · addon id: `plugin.video.ultimate-debrid`**

Fen Light+ v2.3.2 codebase verbatim — all debrid features intact:

| Component | Source |
|-----------|--------|
| Cloud scrapers (5) | `rd_cloud`, `pm_cloud`, `ad_cloud`, `tb_cloud`, `oc_cloud` — from Fen Light+ |
| Direct scrapers (2) | `easynews`, `folders` — from Fen Light+ |
| External provider loader | `external.py` — loads Torrentio/Comet/Knightcrawler from separate addon |
| Debrid APIs (6) | RD, PM, AD, TB, OC, ED — full OAuth wrappers from Fen Light+ |
| Metadata APIs | TMDB, Trakt, IMDB, OMDb — same embedded keys |
| Caching | 9 SQLite databases |
| Entry | `main.py` → `router.py` → mode dispatch |

### Ultimate Non-Debrid — `~/addon-audit/build/ultimate-nondebrid/`

**83 files, 1.4MB · addon id: `plugin.video.ultimate-nondebrid`**

Fen Light+ core stripped of debrid + Umbrella non-debrid scrapers adapted:

| Component | Source |
|-----------|--------|
| Non-debrid scrapers (5) | `easynews` (Fen), `folders` (Fen), `filepursuit` (Umbrella adapted), `gdrive` (Umbrella adapted), `plexshare` (Umbrella adapted) |
| Adapter layer | `scrapers/__init__.py` — auto-detects Interface A vs B and wraps |
| Debrid | **Zero.** All debrid functions stubbed to return `False` |
| Resolver | Direct pass-through: `resolve_sources()` returns `item['url_dl']` or `item['url']` |

**Key modifications to Fen Light+ settings.py for non-debrid build:**
- `active_internal_scrapers()` — only checks `easynews` and `folders` toggles
- `provider_sort_ranks()` — debrid priorities removed, only `easynews`/`folders` ranks
- `scraping_settings()` — debrid highlight colors removed
- `store_resolved_to_cloud()` / `enabled_debrids_check()` / `authorized_debrid_check()` → always return `False`
- `external_scraper_info()` → always returns `None, ''`

**The adapter in `scrapers/__init__.py`:**
```python
def load_scraper(name):
    module = __import__('scrapers.%s' % name, fromlist=['source'])
    source_class = getattr(module, 'source', None)
    if hasattr(source_class, 'results'):
        return source_class  # Interface A — use directly
    elif hasattr(source_class, 'sources'):
        return _create_adapter(name, source_class)  # Interface B — wrap
    return None
```

## DEBRID DEPENDENCY ANALYSIS

**None of these three addons can play torrents without a debrid service.** Here is the exact code path proof:

### Fen Light+ — Hard Block

`sources.py:123-127`:
```python
if self.active_external:
    self.debrid_enabled = debrid_enabled()
    if not self.debrid_enabled:
        return self.disable_external('No Debrid Services Enabled')  # ← HARD STOP
```

Resolve pipeline at `sources.py:779-791`:
```python
if 'cache_provider' in item:
    cache_provider = item['cache_provider']           # 'Uncached Real-Debrid'
    if cache_provider in debrid_providers:             # FALSE — uncached label doesn't match
        url = self.resolve_cached(...)
    # Falls through. url stays None. Source silently skipped.
```

### Seren — Only 3 Debrid Resolvers

`resolver/__init__.py:36-40`:
```python
self.resolvers = {
    "all_debrid": AllDebridResolver,
    "premiumize": PremiumizeResolver,
    "real_debrid": RealDebridResolver,
}
```

### Umbrella — Premium Provider Gate

`sources.py:66-68`:
```python
if not self.prem_providers:
    return control.notification(message=33034)  # "No sources found"
```

### Non-Debrid Streaming Alternatives

| Source Type | How It Works | Addons |
|-------------|-------------|--------|
| **Easynews** (Usenet) | Direct HTTP stream URLs | Fen Light+, Umbrella |
| **Folder scrapers** (SMB/NFS) | Direct file paths | Fen Light+ |
| **GDrive** | Google Drive direct download links | Umbrella |
| **FilePursuit** | Direct download link search engine | Umbrella |
| **PlexShare** | Plex relay streaming | Umbrella |

True P2P torrent streaming (no debrid) requires separate tools:
- **Elementum** — built-in Go torrent engine in Kodi (libtorrent-based P2P)
- **Jacktook** — hybrid: Stremio addons + Jackett/Prowlarr + TorrServer + debrid
- **TorrServer** — standalone C++ torrent engine with Kodi frontends

## COMPLETE EMBEDDED API KEYS

### Fen Light+ (v2.3.2)

| Key | Value | Location |
|-----|-------|----------|
| **TMDB API Key** | `7e4d48e68abb9a4f2c86a1cc143cd8b5` | `kodi_utils.py:21` |
| **TMDB Read Access Token** | `eyJhbG...2-po` (JWT truncated in source) | `settings_cache.py:300` |
| **Trakt Client ID** | `17d84db50cb16624adc67401637f0995949fb8dce2110100d0cb6c0750d273b3` | `kodi_utils.py:22` |
| **Trakt Client Secret** | `a69c7bb0bad9774f6a216a306410bd303d7e92ad92c67c057b0389ad99b8491a` | `kodi_utils.py:23` |
| **Premiumize Client ID** | `888228107` | `premiumize_api.py:19` |
| **Real-Debrid Client ID** | `X245A4XAIBGVM` (fallback) | `real_debrid_api.py:33` |
| **AllDebrid User-Agent** | `Fen Light for Kodi` | `alldebrid_api.py:18` |

### Seren (v3.0.1)

| Key | Value | Location |
|-----|-------|----------|
| **TMDB API Key** | `9f3ca569aa46b6fb13931ec96ab8ae7e` | `indexers/tmdb.py:244` |
| **TVDB API Key** | `43VPI0R8323FB7TI` | `indexers/tvdb.py:1213` |
| **Premiumize Client ID** | `662875953` | `debrid/premiumize.py:25` |
| **Real-Debrid Client ID** | `X245A4XAIBGVM` (via `RD_AUTH_CLIENT_ID`) | `debrid/real_debrid.py:23` |

### Umbrella (v6.7.79)

| Key | Value | Location |
|-----|-------|----------|
| **TMDB API Key** | `edde6b5e41246ab79a2697cd125e1781` (fallback) | `indexers/tmdb.py:62` |
| **SimKL Client ID** | `cecec23773dff71d940876860a316a4b74666c4c31ad719fe0af8bb3064a34ab` | `modules/simkl.py:27` |
| **MDBList Client ID** | `YVFS6WW6GT4c2LvaHxHGa8YlB6u9zGgL6KjtDsHg` | `modules/mdblist.py:43` |

### Key Notes

- **`X245A4XAIBGVM`** is Real-Debrid's generic public OAuth client ID. All three addons use the same ID. During OAuth, the user's own credentials are generated dynamically.
- TMDB keys serve as fallbacks if user hasn't configured their own.
- Trakt keys in Fen Light+ are the only hardcoded default. Seren/Umbrella require user config.

## Debrid Service Integration

| Service | Fen Light+ | Seren | Umbrella |
|---------|-----------|-------|----------|
| Real-Debrid | ✅ Full | ✅ Full | ✅ Full |
| Premiumize | ✅ Full | ✅ Full | ✅ Full |
| AllDebrid | ✅ Full | ✅ Full | ✅ Full |
| OffCloud | ✅ | ❌ | ✅ |
| TorBox | ✅ | ❌ | ✅ |
| EasyDebrid | ✅ | ❌ | ✅ |
| Easynews | ✅ API + scraper | ❌ | ✅ scraper |
| Furk | ❌ (removed in FL) | ❌ | ✅ |

### Real-Debrid API Endpoints

| Endpoint | Purpose |
|----------|---------|
| `POST oauth/v2/device/code` | OAuth device code |
| `POST oauth/v2/device/credentials` | Token exchange |
| `POST /token` | Token refresh |
| `GET /torrents/instantAvailability` | **Cache check (DEPRECATED)** — returns `"disabled_endpoint"` |
| `POST /torrents/addMagnet` | Add magnet → get `status == waiting_files_selection` if cached |
| `POST /torrents/selectFiles` | Select files in torrent |
| `GET /torrents/info/{id}` | Torrent details |
| `POST /unrestrict/link` | Convert to streamable HTTPS URL |
| `DELETE /torrents/delete/{id}` | Cleanup |
| `GET /torrents` | List active torrents (cloud) |
| `GET /downloads` | List completed downloads |
| `GET /hosts/domains` | Supported hoster domains |
| `GET /hosts/regex` | Hoster URL regex patterns |

**Cache check workaround (since instantAvailability was deprecated)**: `POST /torrents/addMagnet` each hash individually, check if response status is `"waiting_files_selection"` (= cached). Then `DELETE /torrents/delete/{id}`. Batch 3-5 concurrent to avoid rate limits.

### Debrid Cache Check Flow

```
hash_list → check LOCAL SQLite cache (hash → {cached: bool, debrid: string})
  → for uncached → batch check DEBRID API
    → RD: individual magnet add + check status (deprecated instantAvailability)
    → PM: check_cache → [bool]
    → AD: check_hash → {hash: cached}
    → TB: check_cached → {hash: cached}
    → OC: check_cache → {"cachedItems": [hashes]}
  → store in LOCAL SQLite
```

## Caching Systems

All use SQLite with TTL-based expiry:

| Cache Layer | Fen Light+ | Seren | Umbrella |
|-------------|-----------|-------|----------|
| Metadata | `meta_cache.py` (SQLite, TTL) | `database/trakt_sync/` (SQLite sync) | `metacache.py` (SQLite) |
| Debrid status | `debrid_cache.py` (hash → bool) | `torrentCache/` (hash → bool) | `providerscache.py` |
| Scraper results | `external_cache.py` (per-provider TTL) | `providerCache/` (SQLite) | `cache.py` (function-hash keyed) |
| Settings | `settings_cache.py` (SQLite+memory) | `settings_cache.py` | Kodi settings (n/a) |
| Artwork | N/A | N/A | `artwork.py`, `fanarttv_cache.py` |

## Source Selection & Filtering

### Quality Detection (same pattern across all three)

Regex-based from release filename:

```python
RES_4K = ('.4k', 'hd4k', '2160p', '.uhd', 'ultrahd', ...)
RES_1080 = ('1080', '1080p', '1080i', 'fullhd', 'full.hd', ...)
RES_720 = ('720', '720p', 'hd720', ...)
CAM = ('.cam.', 'camrip', 'hdcam', ...)
SCR = ('.scr.', 'screener', 'dvdscr', ...)
TELE = ('.tc.', '.ts.', 'telesync', 'telecine', ...)
```

### Filter Types

- **Quality**: 4K/1080p/720p/SD/CAM/SCR/TELE
- **Codec**: HEVC, H264, XVID, AV1, DIVX
- **HDR**: HDR10, Dolby Vision, HDR10+
- **Audio**: DD, DD+, DTS, DTS-HD, TrueHD, Atmos, AAC, OPUS
- **Source**: REMUX, BluRay, WEB-DL, WEBRip, HDRip, DVD
- **Features**: 3D, IMAX, Multi-lang, Subtitles
- **Exclusions**: Sample, Extras, Ads

External scraper names used for pack size correction: `'torrentio', 'knightcrawler', 'comet'` (Fen Light+), `'torrentio', 'comet', 'knightcrawler', 'mediafusion', 'selfhosted'` (Umbrella).

## External Provider Isolation (Legal Pattern)

All three keep actual torrent site scrapers as separate packages:

- **Fen Light+**: Providers are separate Kodi addons. Dynamically imported at scrape time from `special://home/addons/{ext_folder}/lib/`.
- **Seren**: Providers are zip packages downloaded to `providerModules/`. Built-in manager for install/uninstall/update.
- **Umbrella**: Same pattern as Fen. Also has separate CocoScrapers package.

**Update mechanism** (Fen Light+): Checks `https://github.com/{user}/{repo}/raw/main/packages/fen_light_version` for version. Downloads from same repo.

## External Engines (Non-Debrid P2P)

| Engine | Description | Platform |
|--------|-------------|----------|
| **Elementum** | Built-in Go libtorrent engine. Full P2P streaming in Kodi. Binary downloaded at runtime from `elgatito/elementum-binaries`. Go daemon listens on `127.0.0.1:65220`. | All Kodi |
| **Torrest** | Lightweight C++ BitTorrent engine | Linux/Docker |
| **Jacktorr** | Advanced TorrServer wrapper. Part of Jacktook addon. | Android/Docker |
| **TorrServer** | Standalone C++ torrent stream server. HTTP API. Kodi frontends via Jacktook. | All platforms |

These are **not compatible** with the Fen/Seren/Umbrella scraper pipeline. They have their own scraping and playback architecture.

### TorrServer Integration (Built June 2026)

A TorrServer P2P engine was integrated into the non-debrid combined addon at `~/addon-audit/build/ultimate-nondebrid/`. Two files were added:

**`modules/torrserver_api.py`** — Pure Python HTTP client for TorrServer:
- `TorrServerAPI(host, port)` — constructor with optional auth
- `alive` — health check (GET /echo)
- `add_magnet(magnet)` → torrent hash (POST /torrents)
- `add_torrent(path)` → upload .torrent file
- `torrents()` → list all active torrents
- `get_torrent_info(hash)` → detailed info
- `remove_torrent(hash)` → cleanup
- `get_stream_url(hash, file_id=0)` → HTTP stream URL
- `play_torrent(hash, file_id=0)` → direct play URL
- `get_files(hash)` → file listing from torrent

**`scrapers/torrserver.py`** — Dual search source:
- **DMM/Zilean API** (primary, no setup): `https://zilean.elfhosted.com/dmm/filtered` — searches a public database of torrent hashes. Free, no account required. Returns `[{infoHash, filename, size}]`.
- **Jackett API** (optional, user self-hosts): Torznab XML search via user-configured Jackett host/port/apikey. Parses XML for infohash attribute.
- Both return normalized source dicts with `scrape_provider='torrserver'` and `'hash'` field.
- Includes a `resolver` class that checks TorrServer aliveness, adds magnet, and returns play URL.

**Settings keys added**: `torrserver_host` (default `127.0.0.1`), `torrserver_port` (`8090`), `torrserver_user`, `torrserver_pass`, `jackett_host`, `jackett_port` (`9117`), `jackett_apikey`.

**Resolve flow** (`sources.py resolve_sources()`):
```python
if scrape_provider == 'torrserver' and item.get('hash'):
    ts = TorrServerAPI(host, port, user, pass)
    if ts.alive:
        ts.add_magnet(f'magnet:?xt=urn:btih:{item["hash"]}')
        url = ts.play_torrent(item['hash'], 0)
        return url
```

When building a combined addon this way, add `'torrserver'` to both `internal_include_list` and its module map in `internal_sources()`.

### Web Search UI (Testing Without Kodi)

A pure-Python HTTP server that uses the same search clients (Jackett, DMM) without needing Kodi:

**Location**: `~/addon-audit/search_web.py` — single-file, zero dependencies beyond stdlib.

**Architecture**:
- `http.server.HTTPServer` + `BaseHTTPRequestHandler` — no Flask/Django needed
- Search flow: query -> Jackett Torznab API (if configured) -> DMM Torznab -> results table
- Page template as Python f-string (avoids `%`/`.format()` collision with CSS `{}`)
- Settings page saved as redirect with query params (no session state)
- TorrServer health check via `GET /echo` -> status indicator in footer

**`render_page()` pattern** (avoids template engine issues):
```python
def render_page(self, query, media_type, use_jackett, results_html, ts_alive):
    return f'''<!DOCTYPE html>
...{q}...{ts_s}...'''  # f-string avoids %/.format() escaping problems
```

**Key lessons**:
- CSS `{}` in template strings must be doubled (`{{`/`}}`) when using `.format()` - use f-strings instead
- HTML entities with `%` (`&#9679;`) break `%` formatting - use unicode chars directly (`●`)
- `http.server` processes requests sequentially - fine for dev, add threading for production

**Usage**: `python3 search_web.py [port]` -> opens on `0.0.0.0:8800`.

### Kodi Mock Test Harness

When testing Kodi addon modules outside Kodi, mock the following minimum set:

```python
class MockXBMC:
    ListItem, Player, Monitor as classes + static methods
    getSkinDir, log, translatePath, sleep, executeJSONRPC,
    executebuiltin, getCondVisibility, getInfoLabel

class MockXBMCGUI:
    ListItem, Window, WindowXML, WindowXMLDialog, Dialog,
    DialogProgress as classes + getCurrentWindowId, getCurrentWindowDialogId
```

MockListItem needs: `setLabel`, `setLabel2`, `setPath`, `setIconImage`, `setThumbnailImage`, `setProperty`, `setArt`, `setInfo`, `setMimeType`, `setContentLookup`.

Mount as `sys.modules['xbmc']` and `sys.modules['xbmcgui']` before any addon imports. This covers 95% of Kodi module references in Fen Light+ derived addons.

### DMM/Zilean API Status

The public Zilean endpoint at `zilean.elfhosted.com` was **decommissioned January 2026**. Both the `/dmm/filtered` and `/dmm/search` endpoints return 404/405.

Replacements:
1. **Self-hosted Zilean**: Docker `ipromknight/zilean` - same API, self-managed
2. **DMM Torznab**: `https://zilean.elfhosted.com/torznab/movie?q=TITLE` - Torznab XML format (may also be deprecated)
3. **Jackett/Prowlarr**: Self-hosted indexer aggregator - most reliable, 50+ indexers
4. **ElfHosted Prowlarr**: `http://elfhosted-internal.zilean/torznab/` - requires ElfHosted subscription

### Jacktook Architecture Reference

Jacktook (`plugin.video.jacktook` v1.15.0, `~/addon-audit/downloads/jacktook/`) is a pure Python meta-scraper with two distinct subsystems:

**Torrent search sources:**
- **Jackett** (`lib/clients/jackett.py`): Torznab API client. Searches all indexers with `t=movie` or `t=tvsearch`. 241 lines.
- **Prowlarr** (`lib/clients/prowlarr.py`): Same Torznab protocol, different URL pattern.
- **Stremio addons** (`lib/clients/stremio/`): Full Stremio addon protocol client (303 lines). Discovers addons via `stremio://` URLs, queries stream endpoints, normalizes results. P2P detection via `behavior_hints.p2p` flag in manifest.
- **Torrentio** (`lib/clients/stremio/torrentio.py`): Dedicated Torrentio addon toggling.
- **Zilean/DMM** (`lib/clients/zilean.py`): DMM API for hash lookup.
- **Peerflix** (`lib/clients/peerflix.py`): Peerflix API.
- **Easynews** (`lib/clients/easynews.py`): Usenet search.
- **Jackgram** (`lib/clients/jackgram/`): Telegram channel scraper.
- **Debrid** (`lib/clients/debrid/`): Direct cloud file listings from all supported services.

**P2P playback engines:**
- **Jacktorr** (`lib/api/jacktorr/jacktorr.py`): TorrServer HTTP API wrapper (184 lines). Methods: `add_magnet`, `add_torrent`, `torrents`, `get_torrent_info`, `remove_torrent`, `get_stream_url`, `play_torrent`.
- **Burst providers** (`lib/jacktook/provider_base.py`): Notification-based provider system. External addons starting with `script.jacktook.*` are discovered and called via `xbmc.Monitor.onNotification`. 30-second timeout.

**Domain model**: `TorrentStream` dataclass (`lib/domain/torrent.py`) — 44 lines with 17 fields: `title`, `type`, `indexer`, `infoHash`, `size`, `seeders`, `peers`, `quality`, `url`, `isPack`, `isCached`, `codec`, `hdr`.

### Elementum Architecture Reference

Elementum (`plugin.video.elementum`, `~/addon-audit/downloads/elementum/`) uses a Go daemon (`elementumd`) for P2P streaming:

- **Architecture**: Python Kodi plugin → HTTP calls to `127.0.0.1:65220` → Go daemon handles torrent engine + search + metadata
- **Go binary**: Downloaded at first run from `elgatito/elementum-binaries` (GitHub releases). 11 platform builds (linux_x64, android_arm64, darwin_x64, etc.).
- **Binary mode**: Runs as subprocess OR loaded as shared library via `ctypes.CDLL` and `startWithLogAndArgs()` Go exported function.
- **Provider system**: External Kodi addons implement the Elementum provider API. Go daemon calls them via base64-encoded JSON in Kodi plugin URLs. Methods: `search`, `search_movie`, `search_episode`, `search_season`.
- **Web UI**: Built-in React interface at `resources/web/` (TypeScript source in `resources/web-src/`).
- **Daemon capabilities**: Torrent streaming, TMDB resolution, Trakt sync, search orchestration.

## Vulnerability Notes

1. **No input sanitization** in URL router params — source data flows through `exec()` via dynamic mode dispatch
2. **Dynamic module loading** — all import from external directories (zip extraction paths, addon directories, `__import__()` with user-controlled module names)
3. **API tokens in plaintext** — stored in Kodi `settings.xml` readable by any process with filesystem access
4. **No certificate pinning** — standard `requests.get()` calls, vulnerable to MITM
5. **TinyURL leak** (Fen Light+) — auth QR codes go through `tinyurl.com/api-create.php`, leaking RD auth flow URL to a third-party URL shortener
6. **Bare except:pass** — Umbrella has the most bare exception handlers that silently swallow errors

## Source Code Locations

Audit materials at `/home/rurouni/addon-audit/`:
- `fenlightplus/plugin.video.fenlight/` — Fen Light+ v2.3.2
- `seren/` — Seren v3.0.1
- `umbrella-repo/piers/plugin.video.umbrella/` — Umbrella v6.7.79
- `build/ultimate-debrid/` — Combined debrid addon (Fen+ copy)
- `build/ultimate-nondebrid/` — Combined non-debrid addon (Fen+ + Umbrella adapted)
