Update ytdlp_handler.py
This commit is contained in:
@@ -1,10 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
|
||||||
YouTube-DLP Integration Module.
|
|
||||||
|
|
||||||
Provides async wrappers for searching and downloading content via yt-dlp.
|
|
||||||
Includes parsing logic for yt-dlp's JSON output and progress updates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -18,10 +12,8 @@ import logging
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SearchResult:
|
class SearchResult:
|
||||||
"""Represents a single search result from yt-dlp."""
|
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
duration: float
|
duration: float
|
||||||
@@ -32,7 +24,6 @@ class SearchResult:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_str(self) -> str:
|
def duration_str(self) -> str:
|
||||||
"""Formats duration seconds into HH:MM:SS string."""
|
|
||||||
try:
|
try:
|
||||||
total_seconds = int(self.duration) if self.duration else 0
|
total_seconds = int(self.duration) if self.duration else 0
|
||||||
|
|
||||||
@@ -61,10 +52,8 @@ class SearchResult:
|
|||||||
'view_count': self.view_count
|
'view_count': self.view_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DownloadProgress:
|
class DownloadProgress:
|
||||||
"""Real-time status of an active download."""
|
|
||||||
filename: str
|
filename: str
|
||||||
status: str
|
status: str
|
||||||
percent: float = 0.0
|
percent: float = 0.0
|
||||||
@@ -72,9 +61,7 @@ class DownloadProgress:
|
|||||||
eta: str = ""
|
eta: str = ""
|
||||||
error: str = ""
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
class YtDlpHandler:
|
class YtDlpHandler:
|
||||||
"""Manages yt-dlp subprocesses."""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -94,7 +81,6 @@ class YtDlpHandler:
|
|||||||
self._active_downloads: Dict[str, asyncio.subprocess.Process] = {}
|
self._active_downloads: Dict[str, asyncio.subprocess.Process] = {}
|
||||||
|
|
||||||
def _find_ytdlp(self) -> str:
|
def _find_ytdlp(self) -> str:
|
||||||
"""Locates the yt-dlp binary in the system path."""
|
|
||||||
for name in ['yt-dlp', 'yt-dlp.exe', 'youtube-dl']:
|
for name in ['yt-dlp', 'yt-dlp.exe', 'youtube-dl']:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -111,7 +97,6 @@ class YtDlpHandler:
|
|||||||
raise RuntimeError("yt-dlp not found. Install with: pip install yt-dlp")
|
raise RuntimeError("yt-dlp not found. Install with: pip install yt-dlp")
|
||||||
|
|
||||||
async def search(self, query: str, source: str = "youtube") -> List[SearchResult]:
|
async def search(self, query: str, source: str = "youtube") -> List[SearchResult]:
|
||||||
"""Performs a non-download search using yt-dlp's internal search operators."""
|
|
||||||
search_prefix = {
|
search_prefix = {
|
||||||
"youtube": "ytsearch",
|
"youtube": "ytsearch",
|
||||||
"soundcloud": "scsearch",
|
"soundcloud": "scsearch",
|
||||||
@@ -187,7 +172,6 @@ class YtDlpHandler:
|
|||||||
url: str,
|
url: str,
|
||||||
progress_callback: Optional[Callable[[DownloadProgress], None]] = None
|
progress_callback: Optional[Callable[[DownloadProgress], None]] = None
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Downloads audio and converts it to Opus."""
|
|
||||||
output_template = str(self.download_directory / "%(title)s.%(ext)s")
|
output_template = str(self.download_directory / "%(title)s.%(ext)s")
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -229,7 +213,6 @@ class YtDlpHandler:
|
|||||||
if progress:
|
if progress:
|
||||||
progress_callback(progress)
|
progress_callback(progress)
|
||||||
|
|
||||||
# Parse stdout to find where the file is being saved
|
|
||||||
if '[download] Destination:' in line:
|
if '[download] Destination:' in line:
|
||||||
output_file = line.split('Destination:', 1)[1].strip()
|
output_file = line.split('Destination:', 1)[1].strip()
|
||||||
elif 'has already been downloaded' in line:
|
elif 'has already been downloaded' in line:
|
||||||
@@ -246,7 +229,6 @@ class YtDlpHandler:
|
|||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
if not output_file or not os.path.exists(output_file):
|
if not output_file or not os.path.exists(output_file):
|
||||||
# Fallback: check most recent file in download dir
|
|
||||||
recent_files = sorted(
|
recent_files = sorted(
|
||||||
self.download_directory.glob('*.opus'),
|
self.download_directory.glob('*.opus'),
|
||||||
key=lambda x: x.stat().st_mtime,
|
key=lambda x: x.stat().st_mtime,
|
||||||
@@ -273,7 +255,6 @@ class YtDlpHandler:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_progress(self, line: str) -> Optional[DownloadProgress]:
|
def _parse_progress(self, line: str) -> Optional[DownloadProgress]:
|
||||||
"""Parses standard yt-dlp stdout progress lines."""
|
|
||||||
percent_match = re.search(r'(\d+\.?\d*)%', line)
|
percent_match = re.search(r'(\d+\.?\d*)%', line)
|
||||||
speed_match = re.search(r'at\s+(\S+/s)', line)
|
speed_match = re.search(r'at\s+(\S+/s)', line)
|
||||||
eta_match = re.search(r'ETA\s+(\S+)', line)
|
eta_match = re.search(r'ETA\s+(\S+)', line)
|
||||||
|
|||||||
Reference in New Issue
Block a user