diff --git a/.env.example b/.env.example index 8e3c37d..54b1a8e 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,6 @@ WEBSHARE_PASSWORD= # DOWNLOAD_PATH=/downloads/webshare # MEDIA_ROOT=/data # MAX_CONCURRENT_DOWNLOADS=2 - -# UI downloads → Jellyfin Movies library (container path) -# MOVIES_PATH=/data/Movies # DOWNLOAD_UID=1000 # DOWNLOAD_GID=1000 +# Staging only — Sonarr/Radarr move into Shows/Movies on import. diff --git a/README.md b/README.md index 45e0154..133805f 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,9 @@ Environment variables (required unless noted): | `WEBSHARE_PASSWORD` | _(empty)_ | Webshare password (plain; hashed client-side). Optional with username for VIP/session downloads | | `PORT` | `3001` | HTTP listen port | | `BASE_URL` | `http://localhost:$PORT` | Public base URL Sonarr must reach (use the Compose service hostname when linking containers) | -| `DOWNLOAD_PATH` | `/downloads/webshare` | Where the fake qBittorrent writes files | -| `MEDIA_ROOT` | `/data` | Media root (used for path reporting) | -| `MOVIES_PATH` | `$MEDIA_ROOT/Movies` | UI / direct grabs write into this library tree (Jellyfin folders) | +| `DOWNLOAD_PATH` | `/downloads/webshare` | Staging dir for UI + *arr (Sonarr/Radarr import from here into Shows/Movies) | +| `MEDIA_ROOT` | `/data` | Media root (path reporting) | | `DOWNLOAD_UID` / `DOWNLOAD_GID` | `1000` | chown finished files when the container runs as root | -| `MOVIES_PATH` | `$MEDIA_ROOT/Movies` | UI / direct grabs write into this library tree (Jellyfin folders) | -| `DOWNLOAD_UID` / `DOWNLOAD_GID` | `1000` | chown finished files when container runs as root | | `MAX_CONCURRENT_DOWNLOADS` | `2` | Parallel stream downloads | | `SONARR_URL` | `http://sonarr:8989` | Optional; Sonarr base URL | | `SONARR_API_KEY` | _(empty)_ | Optional; enables periodic missing-episode search | diff --git a/src/qbt.js b/src/qbt.js index eaba407..6686a21 100644 --- a/src/qbt.js +++ b/src/qbt.js @@ -7,10 +7,8 @@ const multer = require('multer'); const { extractWebseed, extractInfoHash } = require('./torrent'); const MEDIA_ROOT = process.env.MEDIA_ROOT || '/data'; -// Staging for *arr (Sonarr/Radarr) import pipeline +// Single staging dir for UI + *arr. Sonarr/Radarr import into Shows/Movies. const SAVE_PATH = process.env.DOWNLOAD_PATH || path.join(MEDIA_ROOT, 'webshare'); -// UI / direct movie downloads land in the Jellyfin Movies library -const MOVIES_PATH = process.env.MOVIES_PATH || path.join(MEDIA_ROOT, 'Movies'); const FILE_UID = parseInt(process.env.DOWNLOAD_UID || '1000', 10); const FILE_GID = parseInt(process.env.DOWNLOAD_GID || '1000', 10); const upload = multer({ storage: multer.memoryStorage() }); @@ -22,27 +20,6 @@ function safeFileName(name) { .trim() || 'download.bin'; } -/** - * Build a Jellyfin-friendly movie folder name from a release filename. - * e.g. "Predator 1987 1080p CZ DAB.mkv" → "Predator (1987)" - */ -function movieFolderFromName(name) { - let base = safeFileName(name).replace(/\.[^.]+$/i, ''); - base = base.replace(/[\[\(][^\]\)]*[\]\)]/g, ' '); - base = base.replace( - /\b(1080p|720p|2160p|4k|uhd|hdr|bluray|blu-ray|webrip|web-dl|hdtv|bdrip|dvdrip|x264|x265|h264|h265|hevc|avc|aac|ac3|dts|truehd|atmos|remux|repack|proper|extended|directors?\.?\s*cut|theatrical|remaster(ed)?|multi|cz|cze|cestina|cesky|dab|dabing|titulky|eng|en|sk|slovak|subs?)\b/gi, - ' ', - ); - base = base.replace(/[._]+/g, ' ').replace(/\s+/g, ' ').trim(); - const yearMatch = base.match(/\b((?:19|20)\d{2})\b/); - if (yearMatch) { - const year = yearMatch[1]; - const title = base.replace(year, ' ').replace(/\s+/g, ' ').trim(); - return `${title || 'Movie'} (${year})`; - } - return base || 'Unknown'; -} - function entryDir(entry) { return entry.saveDir || SAVE_PATH; } @@ -67,7 +44,6 @@ function fixOwnership(filePath) { && Number.isFinite(FILE_UID) && Number.isFinite(FILE_GID) && fs.existsSync(filePath)) { fs.chownSync(filePath, FILE_UID, FILE_GID); - // also chown parent movie folder try { fs.chownSync(path.dirname(filePath), FILE_UID, FILE_GID); } catch { /* ignore */ } } } catch { /* ignore */ } @@ -102,7 +78,6 @@ function saveState() { progress: e.progress, state: e.state, category: e.category, webseedUrl: e.webseedUrl, saveDir: e.saveDir, - library: e.library, })); fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch { } @@ -147,8 +122,7 @@ function loadState() { } function cleanupAfterDownload(entry) { - // Library-bound downloads (UI → Movies/) stay on disk for Jellyfin — do not delete. - if (entry.library) return; + // After Sonarr/Radarr hardlink-import, drop the staging copy. const fpath = entryPath(entry); let attempts = 0; const check = setInterval(() => { @@ -350,7 +324,6 @@ function streamToFile(url, entry) { function register(app) { ensureDir(SAVE_PATH); - ensureDir(MOVIES_PATH); // ── auth ────────────────────────────────────────────────────────────── app.post('/api/v2/auth/login', (req, res) => { @@ -554,35 +527,31 @@ function getQueue() { state: e.state, speed: e.speed || '', path: entryPath(e), - library: Boolean(e.library), })); } /** - * UI / manual grab: write into Movies/