Metadata-Version: 2.4
Name: vidsrc-dlp
Version: 0.5.0
Summary: CLI to search TMDB, resolve VidSrc streams, and download via yt-dlp
Author: Jeevan
License-Expression: MIT
Project-URL: Homepage, https://github.com/tecknet-gg/vidsrc-dlp
Project-URL: Source, https://github.com/tecknet-gg/vidsrc-dlp
Project-URL: BugTracker, https://github.com/tecknet-gg/vidsrc-dlp/issues
Keywords: vidsrc,tmdb,yt-dlp,video,downloader
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: requests>=2.31.0
Requires-Dist: yt-dlp>=2024.0.0
Dynamic: license-file

# vidsrc-dlp

**DISCLAIMER:**

**This project was vibecoded (using Opencode), to a fit a niche that I had in my personal Jellyfin setup. The code here is as is, and by using it you accept the risks of running unverified, AI generated code. Maybe I'll rewrite it myself if I get time.**

---

Search TMDB, resolve a playable HLS stream via VidSrc's 4-hop chain, and download via yt-dlp.

## Install

```bash
pip install vidsrc-dlp
cp .env.example .env  # add your TMDB_API_KEY
```

Or for local development:

```bash
git clone https://github.com/tecknet-gg/vidsrc-dlp
cd vidsrc-dlp
python -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env  # add your TMDB_API_KEY
```

## CLI Reference

### General

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `query` | `str` | *required* | Movie or TV show title |
| `--type` | `movie` / `tv` | `movie` | Media type |
| `--year` | `int` | — | Filter by release / air year |
| `--quality` | `str` | `best` | Target resolution. `best` = highest available. E.g. `1080p`, `720p` |
| `--season` | `int` | — | Season number (required for TV) |
| `--episode` | `int` | — | Episode number (required for TV) |
| `--movies-dir` | `str` | from `.env` | Override movie output directory |
| `--tv-dir` | `str` | from `.env` | Override TV output directory |
| `--no-confirm` | flag | — | Skip download confirmation prompt |
| `--verbose` | flag | — | Enable debug logging |
| `--verbose-ytdlp` | flag | — | Show yt-dlp output |

### Examples

```bash
# Simple movie download
vidsrc-dlp "Inception"
vidsrc-dlp "The Matrix" --year 1999

# Custom quality + skip prompt
vidsrc-dlp "Interstellar" --quality 1080p --no-confirm

# TV episode
vidsrc-dlp "Breaking Bad" --type tv --season 1 --episode 1

# Custom output directories
vidsrc-dlp "Inception" --movies-dir "/Volumes/Media/Movies"
vidsrc-dlp "Breaking Bad" --type tv --season 1 --episode 1 --tv-dir "/Volumes/Media/TV"

# Debug mode
vidsrc-dlp "Inception" --verbose --no-confirm
```

### Quality selection

When `--quality best` (the default), the stream is inspected with yt-dlp before downloading and the highest resolution variant is auto-selected. The detected resolutions are logged so you can see what's available:

```
[INFO] Available qualities: 1080p, 720p, 480p, 360p
[INFO] Format: bestvideo+bestaudio/best
```

Pass a specific height via `--quality 1080p` or `--quality 720p` to override and constrain the selection.

### `.env` configuration

```
TMDB_API_KEY=your_api_key_here
MOVIES_DIR=./downloads/movies
TV_DIR=./downloads/tv
```

CLI flags `--movies-dir` and `--tv-dir` override the `.env` values when provided.

---

## Inline API (Python)

### Public functions

#### `search_movie(query, year=None, api_key=None) → list[Media]`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `query` | `str` | — | Movie title to search for |
| `year` | `int` | `None` | Filter by release year |
| `api_key` | `str` | `None` | TMDB key (falls back to `.env`) |

```python
>>> from vidsrc_dlp import search_movie
>>> movies = search_movie("Inception", year=2010)
>>> movies[0].title
'Inception'
```

---

#### `search_tv(query, year=None, api_key=None) → list[Media]`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `query` | `str` | — | TV show title to search for |
| `year` | `int` | `None` | Filter by first air year |
| `api_key` | `str` | `None` | TMDB key (falls back to `.env`) |

```python
>>> from vidsrc_dlp import search_tv
>>> shows = search_tv("Breaking Bad")
>>> shows[0].title
'Breaking Bad'
```

---

#### `get_movie_details(tmdb_id, api_key=None) → Media | None`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `tmdb_id` | `int` | — | TMDB movie ID |
| `api_key` | `str` | `None` | TMDB key (falls back to `.env`) |

Returns enriched metadata (IMDb ID, genres, rating, poster path).

```python
>>> from vidsrc_dlp import get_movie_details
>>> m = get_movie_details(27205)
>>> m.imdb_id
'tt1375666'
>>> m.genres
['Action', 'Science Fiction', 'Adventure']
>>> m.vote_average
8.4
```

---

#### `get_tv_details(tmdb_id, api_key=None) → Media | None`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `tmdb_id` | `int` | — | TMDB TV show ID |
| `api_key` | `str` | `None` | TMDB key (falls back to `.env`) |

```python
>>> from vidsrc_dlp import get_tv_details
>>> show = get_tv_details(1396)
>>> show.imdb_id
'tt0903747'
```

---

#### `resolve(media, season=None, episode=None) → StreamInfo | None`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `media` | `Media` | — | Movie or TV show to resolve |
| `season` | `int` | `None` | Season (reads from `media.season`) |
| `episode` | `int` | `None` | Episode (reads from `media.episode`) |

```python
>>> from vidsrc_dlp import search_movie, resolve
>>> movie = search_movie("Inception")[0]
>>> stream = resolve(movie)
>>> stream.url.startswith("http")
True
```

---

#### `download(stream, media, quality="best", movies_dir=None, tv_dir=None) → bool`

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `stream` | `StreamInfo` | — | Stream from `resolve()` |
| `media` | `Media` | — | Movie or TV show metadata |
| `quality` | `str` | `best` | Target resolution. `best` = highest available. E.g. `1080p`, `720p` |
| `movies_dir` | `str` | from `.env` | Override movie output path |
| `tv_dir` | `str` | from `.env` | Override TV output path |

```python
>>> from vidsrc_dlp import search_movie, resolve, download
>>> movie = search_movie("Inception")[0]
>>> stream = resolve(movie)
>>> download(stream, movie)
True
```

---

### Data types

#### `Media`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `int` | TMDB ID |
| `title` | `str` | Movie or show name |
| `media_type` | `MediaType` | `MOVIE` or `TV` |
| `year` | `int \| None` | Release / air year |
| `release_date` | `str` | Full date string |
| `overview` | `str` | Plot synopsis |
| `season` | `int \| None` | Season number (for TV) |
| `episode` | `int \| None` | Episode number (for TV) |
| `episode_title` | `str \| None` | Episode name |
| `imdb_id` | `str \| None` | IMDb ID (after `get_*_details()`) |
| `genres` | `list[str]` | Genre names |
| `vote_average` | `float \| None` | TMDB rating |
| `poster_path` | `str \| None` | TMDB poster path |

#### `StreamInfo`

| Field | Type | Description |
|-------|------|-------------|
| `url` | `str` | Resolved HLS m3u8 URL |
| `headers` | `dict[str, str]` | HTTP headers for download (`User-Agent`) |
| `referer` | `str` | Referer URL required by CDN |
| `stream_type` | `str` | Always `"hls"` |

#### `MediaType` (enum)

```
MediaType.MOVIE
MediaType.TV
```

---

### Full workflow example

```python
from vidsrc_dlp import (
    search_movie, get_movie_details, resolve, download,
    search_tv, get_tv_details, MediaType, Media,
)

# === Movie ===
movies = search_movie("Inception", year=2010)      # → list[Media]
movie = get_movie_details(movies[0].id)             # → Media (enriched)
print(movie.imdb_id, movie.genres, movie.year)      # tt1375666, [...]

stream = resolve(movie)                              # → StreamInfo
download(stream, movie, quality="1080p")             # → bool

# === TV ===
shows = search_tv("Breaking Bad")
show = get_tv_details(shows[0].id)
show.season = 1                                       # set before resolve()
show.episode = 1
show.episode_title = "Pilot"

stream = resolve(show)
download(stream, show)

# === Pass api_key inline (skip .env) ===
movies = search_movie("Inception", api_key="your_key")
stream = resolve(movies[0])
download(stream, movies[0], movies_dir="/custom/path")
```

## Architecture

1. **TMDB** — search API for movie/TV metadata
2. **VidSrc Resolver** — `requests`-only 4-hop chain to extract an HLS URL
3. **Downloader** — `yt-dlp` + ffmpeg to download and transcode to MP4

## How the resolver works

```
vidsrc.to/embed/{movie|tv}/{tmdb_id}[/{season}/{episode}]
  → vsembed.ru iframe (parse src attr)
    → cloudorchestranova.com/rcp/{hash} (parse prorcp hash)
      → cloudorchestranova.com/prorcp/{hash} (m3u8 URLs with __TOKEN__ placeholders)
        → generate.php endpoints (JWT tokens)
          → final m3u8 URL
```

## Adding a new stream provider

Implement `StreamProvider` from `vidsrc_dlp.utils`.

## Acknowledgements

- **[MaheshSharan/vidsrc](https://github.com/MaheshSharan/vidsrc)** — The request-based 4-hop resolution chain was reverse-engineered from this open-source PHP scraper.
- **[TMDB](https://www.themoviedb.org/)** — Movie and TV metadata API.
- **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** — Download engine for HLS fetching, decryption, and ffmpeg transcoding.
