diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html index a15bb4c..815d89a 100644 --- a/qbittorrent_performer_sorter.html +++ b/qbittorrent_performer_sorter.html @@ -4002,7 +4002,7 @@ Return ONLY a valid JSON object with an array named "results": renderCurrentVideoPreview(); } - function openVideoPreviewForTorrent(hash) { + async function openVideoPreviewForTorrent(hash) { const query = searchInput.value.trim().toLowerCase(); const filtered = qbState.torrents.filter(t => { if (!query) return true; @@ -4012,6 +4012,11 @@ Return ONLY a valid JSON object with an array named "results": const idx = filtered.findIndex(t => t.hash === hash); if (idx === -1) return; + const item = filtered[idx]; + if (!item.innerFiles || item.innerFiles.length === 0) { + item.innerFiles = await fetchTorrentFiles(item.hash); + } + previewState.activeType = 'qb'; previewState.currentList = filtered; previewState.currentIndex = idx; @@ -4034,8 +4039,17 @@ Return ONLY a valid JSON object with an array named "results": streamPath = item.path; } else { // qb torrent - const base = (item.save_path || '/downloads').replace(/\/+$/, ''); - streamPath = `${base}/${item.name}`; + if (item.innerFiles && item.innerFiles.length > 0) { + const videoFile = item.innerFiles.find(f => /\.(mp4|mkv|avi|mov|webm|m4v|ts)$/i.test(f.name)); + if (videoFile) { + const base = (item.save_path || '/downloads').replace(/\/+$/, ''); + streamPath = `${base}/${videoFile.name}`; + } + } + if (!streamPath) { + const base = (item.save_path || '/downloads').replace(/\/+$/, ''); + streamPath = `${base}/${item.name}`; + } } videoTitle.textContent = `${previewState.activeType === 'smb' ? 'SMB Video' : 'Torrent'}: ${titleName}`; diff --git a/server.py b/server.py index 5b14274..53db802 100644 --- a/server.py +++ b/server.py @@ -427,6 +427,91 @@ def ensure_smb_mounted(): return False, f"Auto-mount error: {str(e)}" return os.path.exists(DEFAULT_SMB_MOUNT), "Mount status checked" +# ==================== Video Streaming Helper (Range 206) ==================== +def resolve_stream_video_path(requested_path): + if not requested_path: + return None + ensure_smb_mounted() + + # 1. Direct path check + if os.path.isfile(requested_path): + return requested_path + + basename = os.path.basename(requested_path) + stem = os.path.splitext(basename)[0] + + candidate_paths = [ + requested_path.replace('/isolation/videos/', '/mnt/isolation/videos/'), + requested_path.replace('/isolation/', '/mnt/isolation/'), + requested_path.replace('/downloads/', '/mnt/isolation/videos/unsorted/'), + requested_path.replace('/downloads/', '/mnt/isolation/videos/videodownloader/'), + requested_path.replace('/downloads/', '/mnt/isolation/videos/'), + requested_path.replace('/downloads/', '/mnt/isolation/incomplete/'), + requested_path.replace('/downloads/', '/mnt/isolation/downloads/'), + os.path.join('/mnt/isolation/videos/unsorted', basename), + os.path.join('/mnt/isolation/videos/videodownloader', basename), + os.path.join('/mnt/isolation/incomplete', basename), + os.path.join('/mnt/isolation/downloads', basename) + ] + + video_exts = ('.mp4', '.mkv', '.avi', '.mov', '.webm', '.ts', '.m4v') + + for p in candidate_paths: + if os.path.isfile(p): + return p + elif os.path.isdir(p): + try: + best_file = None + best_size = -1 + for root, _, files in os.walk(p): + for f in files: + if f.lower().endswith(video_exts): + full = os.path.join(root, f) + sz = os.path.getsize(full) + if sz > best_size: + best_size = sz + best_file = full + if best_file: + return best_file + except Exception: + pass + + # 2. Check top-level directories + try: + search_dirs = [ + '/mnt/isolation/videos/unsorted', + '/mnt/isolation/videos/videodownloader', + '/mnt/isolation/incomplete', + '/mnt/isolation/downloads' + ] + for s_dir in search_dirs: + if os.path.exists(s_dir) and os.path.isdir(s_dir): + target = os.path.join(s_dir, basename) + if os.path.isfile(target): + return target + for entry in os.scandir(s_dir): + if entry.is_file() and (entry.name == basename or os.path.splitext(entry.name)[0] == stem): + return entry.path + elif entry.is_dir(): + sub_target = os.path.join(entry.path, basename) + if os.path.isfile(sub_target): + return sub_target + except Exception as e: + print(f"Error scanning directories in streaming resolver: {e}") + + # 3. Fallback search inside performer folders in /mnt/isolation/videos + try: + if os.path.exists('/mnt/isolation/videos'): + for p_entry in os.scandir('/mnt/isolation/videos'): + if p_entry.is_dir(): + candidate = os.path.join(p_entry.path, basename) + if os.path.isfile(candidate): + return candidate + except Exception: + pass + + return None + class RobustProxyHandler(http.server.SimpleHTTPRequestHandler): def send_cors_headers(self): origin = self.headers.get('Origin') @@ -699,11 +784,13 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler): self.wfile.write(json.dumps({"error": f"Failed batch saving to DB: {str(e)}"}).encode('utf-8')) # ==================== Video Streaming Handlers (Range 206) ==================== - def handle_stream_video(self, head_only=False): + def handle_stream_video(self): + head_only = False parsed = urllib.parse.urlparse(self.path) params = urllib.parse.parse_qs(parsed.query) - file_path = params.get('path', [''])[0] + raw_path = params.get('path', [''])[0] + file_path = resolve_stream_video_path(raw_path) if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path): self.send_response(404) self.send_cors_headers()