Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,3 @@ build/
# OS
.DS_Store
Thumbs.db

9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
TMDB_API_KEY=<str>
TMDB_API_KEY=<your_tmdb_api_key>
PORT=8000
ADDON_ID=com.bimal.watchly
ADDON_NAME=Watchly
REDIS_URL=redis://redis:6379/0
TOKEN_SALT=replace-with-a-long-random-string
TOKEN_TTL_SECONDS=0
ANNOUNCEMENT_HTML=
5 changes: 2 additions & 3 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
patreon: TimilsinaBimal
ko-fi: TimilsinaBimal
custom: ["https://www.paypal.com/donate/?hosted_button_id=KRQMVS34FC5KC"]
ko_fi: TimilsinaBimal
custom: ["https://www.paypal.com/donate/?hosted_button_id=KRQMVS34FC5KC"]
29 changes: 29 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Linter

# Enable Buildkit and let compose use it to speed up image building
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1

on:
pull_request:
push:

concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
linter:
runs-on: ubuntu-latest
steps:
- name: Checkout Code Repository
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Run pre-commit
uses: pre-commit/action@v3.0.0
45 changes: 45 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
default_stages: [pre-commit]
exclude: '^misc/|^data/|^docs/'

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
- id: check-toml
- id: check-xml
- id: check-yaml
- id: debug-statements
- id: check-builtin-literals
- id: check-case-conflict
- id: check-docstring-first
- id: detect-private-key

- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
args: [--py311-plus]

- repo: https://github.com/psf/black
rev: 24.4.0
hooks:
- id: black

- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort

- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8

# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date
ci:
autoupdate_schedule: weekly
skip: []
submodules: false
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ COPY static/ ./static/
COPY main.py .
COPY pyproject.toml .

ENTRYPOINT ["python", "main.py"]
ENTRYPOINT ["python", "main.py"]
111 changes: 50 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Watchly
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I81OVJEH)
[![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/donate/?hosted_button_id=KRQMVS34FC5KC)
# Watchly

**Watchly** is a Stremio catalog addon that provides personalized movie and series recommendations based on your Stremio library. It uses The Movie Database (TMDB) API to generate intelligent recommendations from the content you've watched and loved.

Expand Down Expand Up @@ -33,7 +33,10 @@ Watchly is a FastAPI-based Stremio addon that:
- ✅ **Similar Content Discovery** - find content similar to specific titles
- ✅ **Web Configuration Interface** - easy setup through a web UI
- ✅ **Caching** - optimized performance with intelligent caching
- ✅ **Secure Tokenized Access** - credentials/auth keys never travel in URLs
- ✅ **Docker Support** - easy deployment with Docker and Docker Compose
- ✅ **Background Catalog Refresh** - automatically keeps Stremio catalogs in sync
- ✅ **Credential Validation** - verifies access details and primes catalogs before issuing tokens

## Installation

Expand Down Expand Up @@ -63,7 +66,7 @@ Watchly is a FastAPI-based Stremio addon that:
```
TMDB_API_KEY=your_tmdb_api_key_here
PORT=8000
ADDON_ID=com.bimal.watchly
...
```

4. **Start the application:**
Expand All @@ -76,23 +79,6 @@ Watchly is a FastAPI-based Stremio addon that:
- Configuration page: `http://localhost:8000/configure`
- API Documentation: `http://localhost:8000/docs`

#### Using Docker Only

1. **Build the image:**
```bash
docker build -t watchly .
```

2. **Run the container:**
```bash
docker run -d \
--name watchly \
-p 8000:8000 \
-e TMDB_API_KEY=your_tmdb_api_key_here \
-e PORT=8000 \
-e ADDON_ID=com.bimal.watchly \
watchly
```

### Option 2: Manual Installation

Expand All @@ -102,70 +88,63 @@ Watchly is a FastAPI-based Stremio addon that:
cd Watchly
```

2. **Create a virtual environment (recommended):**
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```

3. **Install dependencies:**
```bash
pip install -r requirements.txt
```

4. **Set environment variables:**

2. **Set environment variables:**
Create a `.env` file in the project root:
```
TMDB_API_KEY=your_tmdb_api_key_here
PORT=8000
ADDON_ID=com.bimal.watchly
```

Or export them in your shell:
```bash
export TMDB_API_KEY=your_tmdb_api_key_here
export PORT=8000
export ADDON_ID=com.bimal.watchly
...
```

5. **Run the application:**
3. **Install UV and Run app (recommended):**
- [Installation Instructions](https://docs.astral.sh/uv/getting-started/installation/)
```bash
uvicorn app.core.app:app --host 0.0.0.0 --port 8000
uv run main.py
```

Or using Python directly:
```bash
python main.py
```

6. **Access the application:**
4. **Access the application:**
- API: `http://localhost:8000`
- Configuration page: `http://localhost:8000/configure`
- API Documentation: `http://localhost:8000/docs`


*You Can also create virtual environment and install dependencies from requirements.txt and run the app*

## Configuration

### Environment Variables

| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `TMDB_API_KEY` | Your TMDB API key | Yes | - |
| `TMDB_API_KEY` | Your TMDB API key | Required for catalog features (optional for `/health`) | *(empty)* |
| `PORT` | Server port | No | 8000 |
| `ADDON_ID` | Stremio addon identifier | No | com.bimal.watchly |
| `ADDON_NAME` | Human-friendly addon name shown in the manifest/UI | No | Watchly |
| `REDIS_URL` | Redis connection string for credential tokens | No | `redis://localhost:6379/0` |
| `TOKEN_SALT` | Secret salt for hashing token IDs | Yes | - (must be set in production) |
| `TOKEN_TTL_SECONDS` | Token lifetime in seconds (`0` = no expiry) | No | 0 |
| `ANNOUNCEMENT_HTML` | Optional HTML snippet rendered in the configurator banner | No | *(empty)* |
| `TMDB_ADDON_URL` | Base URL for the TMDB addon metadata proxy | No | `https://94c8cb9f702d-tmdb-addon.baby-beamup.club/...` |
| `AUTO_UPDATE_CATALOGS` | Enable periodic background catalog refreshes | No | `true` |
| `CATALOG_REFRESH_INTERVAL_SECONDS` | Interval between automatic refreshes (seconds) | No | `21600` (6h) |

### User Configuration

Users configure their Stremio credentials through the web interface at `/configure`. Credentials are:
- Encoded in the addon URL (base64)
- Never stored on the server
- Used only for API requests to Stremio
Use the web interface at `/configure` to provision a secure access token:

1. Provide either your **Stremio username/password** *or* an **existing `authKey`** (copy from `localStorage.authKey` in [https://web.stremio.com/](https://web.stremio.com/)).
2. Choose whether to base recommendations on loved items only or include everything you've watched.
3. Watchly verifies the credentials/auth key with Stremio, performs the first catalog refresh in the background, and only then stores the payload inside Redis.
4. Your manifest URL becomes `https://<host>/<token>/manifest.json`. Only this token ever appears in URLs.
5. Re-running the setup with the same credentials/configuration returns the exact same token.

By default (`TOKEN_TTL_SECONDS=0`), tokens never expire. Set a positive TTL if you want automatic rotation.

## How It Works

1. **User Configuration**: User enters Stremio credentials via web interface
2. **Credential Encoding**: Credentials are base64 encoded and included in the addon URL
3. **Library Fetching**: When catalog is requested, service authenticates with Stremio and fetches user's library
1. **User Configuration**: User submits Stremio credentials or auth key via the web interface
2. **Secure Tokenization**: Credentials/auth keys are stored server-side in Redis; the user only receives a salted token
3. **Library Fetching**: When catalog is requested, service resolves the token, authenticates with Stremio, and fetches the library
4. **Seed Selection**: Uses most recent "loved" items (default: 10) as seed content
5. **Recommendation Generation**: For each seed, fetches recommendations from TMDB
6. **Filtering**: Removes items already in user's watched library
Expand Down Expand Up @@ -216,28 +195,38 @@ Watchly/
### Running in Development Mode

```bash
uvicorn app.core.app:app --reload --host 0.0.0.0 --port 8000
uv run main.py --dev
```

Or using Python directly (with auto-reload based on APP_ENV):
```bash
python main.py
```

### Health Check Endpoint

The `/health` endpoint responds with `{ "status": "ok" }` without touching external services. This keeps container builds and probes green even when secrets like `TMDB_API_KEY` aren't supplied yet.

### Background Catalog Updates

Watchly now refreshes catalogs automatically using the credentials stored in Redis. By default the background worker runs every 6 hours and updates each token's catalogs directly via the Stremio API. To disable the behavior, set `AUTO_UPDATE_CATALOGS=false` (or choose a custom cadence with `CATALOG_REFRESH_INTERVAL_SECONDS`). Manual refreshes through `/{token}/catalog/update` continue to work and reuse the same logic.

### Testing

```bash
# Test manifest endpoint
curl http://localhost:8000/manifest.json

# Test catalog endpoint (requires encoded credentials)
curl http://localhost:8000/{encoded}/catalog/movie/watchly.rec.json
# Test catalog endpoint (requires a credential token)
curl http://localhost:8000/{token}/catalog/movie/watchly.rec.json
```

## Security Notes

- **Credentials in URL**: User credentials are base64 encoded in the addon URL. While encoded, they are not encrypted. Users should be aware of this.
- **HTTPS Recommended**: Always use HTTPS in production to protect credentials in transit.
- **Tokenized URLs**: Manifest/catalog URLs now contain only salted tokens. Credentials/auth keys never leave the server once submitted.
- **Rotate `TOKEN_SALT`**: Treat the salt like any other secret; rotate if you suspect compromise. Changing the salt invalidates all tokens.
- **Redis Security**: Ensure your Redis instance is not exposed publicly and enable authentication if hosted remotely.
- **HTTPS Recommended**: Always use HTTPS in production to protect tokens in transit.
- **Environment Variables**: Never commit `.env` files or expose API keys in code.

## Troubleshooting
Expand Down
10 changes: 10 additions & 0 deletions app/api/endpoints/announcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastapi import APIRouter

from app.config import settings

router = APIRouter(prefix="/announcement", tags=["announcement"])


@router.get("/")
async def get_announcement() -> dict:
return {"html": settings.ANNOUNCEMENT_HTML or ""}
1 change: 1 addition & 0 deletions app/api/endpoints/caching.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException
from loguru import logger

from app.utils import clear_cache

router = APIRouter(prefix="/cache")
Expand Down
Loading