Pages, API routes, database schema, streaming pipeline, recommendation engine, download system, and Cloudflare deployment.
NodWatch is a single Next.js 15 application — not a monorepo. All logic lives under src/. It is deployed to Cloudflare Workers via OpenNext and is live at nodwatch.online.
| Route | Notes |
|---|---|
/ | Home — 8 TMDB carousels (trending/popular × movie/tv/anime/kdrama), hero carousel, ContinueWatching, BecauseYouWatched, AdUnits |
/movies, /tv, /anime, /kdrama | Browse with DiscoverFilters (genre, year, rating) |
/movies/[id], /tv/[id], /anime/[id] | Detail page — credits, cast, similar, trailer modal, rating, watchlist, reviews |
/movies/[id]/watch | Auth-gated watch page with StreamPlayer + DownloadButton |
/tv/[id]/[season]/[episode] | Episode watch page |
/anime/[id]/[season]/[episode] | Anime episode watch — supports absoluteEpisode for cross-season numbering |
/search | Multi-type search with filters |
/person/[id] | Actor/crew detail |
/collection/[id] | Movie collection |
/onboarding | 2-step: pick categories → pick TMDB genres; saves to user_preferences |
/(main)/watchlist, /library, /downloads, /profile | Authenticated user pages |
/offline/[id] | Offline player reading from OPFS |
/feedback, /feedback/new | Bug/feature reports with Cloudinary image upload |
/admin/feedback | Admin inbox — role or email gated via isAdminUser() |
/login, /register, /privacy, /terms, /cookies | Auth and legal pages |
| Route | Purpose |
|---|---|
ALL /api/auth/[...all] | Better-Auth catch-all |
POST /api/stream/token | Auth-gated; builds AES-encrypted 60-second token encoding the embed URL |
GET /api/stream/proxy?token=… | Decrypts token, validates expiry, proxies embed URL server-side |
GET /api/subtitles | OpenSubtitles search by imdbId + language |
POST /api/subtitles | Resolves fileId → download link |
GET/POST/DELETE /api/watchlist | Watchlist CRUD |
POST /api/progress | Upsert watch_progress row |
GET/POST /api/ratings | Per-title star rating |
GET/POST /api/reviews, DELETE /api/reviews/[id] | Reviews CRUD |
GET /api/user/stats | totalWatchlistCount, totalTitlesWatched, totalHoursWatched |
GET /api/user/continue-watching | Incomplete watch_progress rows |
GET/PUT /api/user/preferences | Onboarding preferences |
GET /api/user/notifications, PATCH /api/user/notifications/[id] | Notifications |
GET /api/recommendations | Custom engine result, cached in Cloudflare KV |
GET /api/tmdb/genres | Proxies TMDB genre list |
GET /api/torrents, GET /api/torrents/tv | Torrent search |
POST /api/downloads/prepare | Starts cinepro extraction job, returns jobId |
GET /api/downloads/prepare/[jobId] | Poll job status |
GET /api/downloads/file/[jobId] | Stream extracted file |
GET /api/downloads/events | SSE stream for download progress |
POST /api/downloads/expire-sweep | Called by Cloudflare cron hourly |
POST /api/feedback, GET /api/feedback/my | Submit and list feedback |
POST /api/feedback/upload-signature | Cloudinary signed upload params |
GET /api/admin/feedback, PATCH /api/admin/feedback/[id] | Admin feedback management |
| Table | Key Columns |
|---|---|
users | id, name, email UNIQUE, role (default 'user'), emailVerified, image |
sessions | id, userId→users, token UNIQUE, expiresAt, ipAddress, userAgent |
accounts | id, userId→users, accountId, providerId, accessToken, refreshToken, password |
verifications | id, identifier, value, expiresAt |
watchlist | id uuid, userId, tmdbId int, imdbId, mediaType, title, posterPath; UNIQUE(userId, tmdbId, mediaType) |
watch_progress | id uuid, userId, tmdbId, season int, episode int, progressSeconds, durationSeconds, completed bool; UNIQUE(userId, tmdbId, mediaType, season, episode) |
ratings | id uuid, userId, tmdbId, mediaType, rating int; UNIQUE(userId, tmdbId, mediaType) |
reviews | id uuid, userId, tmdbId, mediaType, content text |
notifications | id uuid, userId, title, subtitle, isRead bool |
user_preferences | id uuid, userId UNIQUE, favoriteCategories jsonb[], favoriteGenreIds jsonb[], onboardingCompleted bool |
feedback_reports | id uuid, userId, type (bug/feature), description, status (new/in_review/planned/done/rejected), priority (low/medium/high/critical) |
feedback_attachments | id uuid, reportId, cloudinaryPublicId, cloudinarySecureUrl |
download_licenses | id uuid, userId, downloadId, titleId, tier (non-drm/drm), status (active/expired/revoked), expiresAt |
POST /api/stream/token with { imdbId, tmdbId, type, season, episode, server, resumeAt, dsLang }getMovieStreamUrl / getTVStreamUrl / getAnimeStreamUrl from src/api/stream/resolver.ts to build an embed URL for one of 5 providersexpiresAt (60s) + userId are AES-encrypted and returned as token<iframe src="/api/stream/proxy?token=…" allowFullScreen />postMessage({ type: 'PLAYER_EVENT', data: { player_progress, player_duration, player_status } })localStorage, and debounce-flushes to POST /api/progress every 15 seconds, on pause, seek, or completionnextEpisodeHref or shows a CTAServer options (stored in localStorage):
| Server | Embed URL |
|---|---|
vaplayer | vaplayer.ru/embed/… |
streamimdb | streamimdb.ru/embed/… |
vidsrc | vidsrc.net/embed/… |
vidsrc.cc | vidsrc.cc/v2/embed/… |
embed.su | embed.su/embed/… |
Anime uses absolute episode numbering on streamimdb, vidsrc, and vaplayer.
Custom engine, no external ML service. Located at src/lib/recommendations/.
rating/10, progress weighted by completion ratio and recencyMAX_RESULTSRECOMMENDATIONS_KV)Full offline pipeline in src/features/downloads/:
POST /api/downloads/prepare calls CINEPRO_API_URL to extract the real media file URL, returns jobIdDownloadManager (singleton) manages concurrent downloads with pause/resume/cancelrecordStore, mediaStore); media bytes stored in OPFShlsEngine.ts parses M3U8 manifests, rewrites segment URLs, downloads segments, reassemblesGET /api/downloads/events SSE stream drives the DownloadTray UIdownload_licenses table tracks expiry; Cloudflare cron calls /api/downloads/expire-sweep hourly/offline/[id] + OfflinePlayer component reads media from OPFS for playback# worker.js — wraps .open-next/worker.js and adds:
export default { fetch, scheduled } # scheduled() → /api/downloads/expire-sweep
# Cron: "0 * * * *" — hourly
Bindings:
| Binding | Type | Purpose |
|---|---|---|
ASSETS | Static assets | .open-next/assets |
NEXT_INC_CACHE_R2_BUCKET | R2 | ISR page + fetch cache |
NEXT_TAG_CACHE_D1 | D1 | Tag-based cache invalidation |
RECOMMENDATIONS_KV | KV | Per-user recommendation cache |
WORKER_SELF_REFERENCE | Self-binding | ISR revalidation queue |
HYPERDRIVE | Hyperdrive | Connection pooling to Neon PostgreSQL (EU West) |
bearer() plugin enabled for cross-origin / Expo clientsnodwatch.online, localhost:3000, localhost:8081, Android emulator addressesisAdminUser() checks user.role === 'admin' OR email in ADMIN_EMAILS env varDATABASE_URL="postgres://..."
TMDB_API_KEY=""
TMDB_BASE_URL="https://api.themoviedb.org/3"
TMDB_IMAGE_BASE="https://image.tmdb.org/t/p"
BETTER_AUTH_SECRET=""
BETTER_AUTH_URL="https://nodwatch.online"
NEXT_PUBLIC_BETTER_AUTH_URL="https://nodwatch.online"
OPENSUBTITLES_API_KEY=""
OPENSUBTITLES_APP_NAME=""
CLOUDINARY_CLOUD_NAME=""
CLOUDINARY_API_KEY=""
CLOUDINARY_API_SECRET=""
CINEPRO_API_URL=""
ADMIN_EMAILS=""
STREAM_ENCRYPTION_KEY="" # AES key for stream token encryption