'commit'
This commit is contained in:
3
DouYin/dy-downloader/cli/__init__.py
Normal file
3
DouYin/dy-downloader/cli/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import main
|
||||
|
||||
__all__ = ['main']
|
||||
164
DouYin/dy-downloader/cli/main.py
Normal file
164
DouYin/dy-downloader/cli/main.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import asyncio
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from config import ConfigLoader
|
||||
from auth import CookieManager
|
||||
from storage import Database, FileManager
|
||||
from control import QueueManager, RateLimiter, RetryHandler
|
||||
from core import DouyinAPIClient, URLParser, DownloaderFactory
|
||||
from cli.progress_display import ProgressDisplay
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger('CLI')
|
||||
display = ProgressDisplay()
|
||||
|
||||
|
||||
async def download_url(url: str, config: ConfigLoader, cookie_manager: CookieManager, database: Database = None):
|
||||
file_manager = FileManager(config.get('path'))
|
||||
rate_limiter = RateLimiter(max_per_second=2)
|
||||
retry_handler = RetryHandler(max_retries=config.get('retry_times', 3))
|
||||
queue_manager = QueueManager(max_workers=int(config.get('thread', 5) or 5))
|
||||
|
||||
original_url = url
|
||||
|
||||
async with DouyinAPIClient(cookie_manager.get_cookies()) as api_client:
|
||||
if url.startswith('https://v.douyin.com'):
|
||||
resolved_url = await api_client.resolve_short_url(url)
|
||||
if resolved_url:
|
||||
url = resolved_url
|
||||
else:
|
||||
display.print_error(f"Failed to resolve short URL: {url}")
|
||||
return None
|
||||
|
||||
parsed = URLParser.parse(url)
|
||||
if not parsed:
|
||||
display.print_error(f"Failed to parse URL: {url}")
|
||||
return None
|
||||
|
||||
display.print_info(f"URL type: {parsed['type']}")
|
||||
|
||||
downloader = DownloaderFactory.create(
|
||||
parsed['type'],
|
||||
config,
|
||||
api_client,
|
||||
file_manager,
|
||||
cookie_manager,
|
||||
database,
|
||||
rate_limiter,
|
||||
retry_handler,
|
||||
queue_manager
|
||||
)
|
||||
|
||||
if not downloader:
|
||||
display.print_error(f"No downloader found for type: {parsed['type']}")
|
||||
return None
|
||||
|
||||
result = await downloader.download(parsed)
|
||||
|
||||
if result and database:
|
||||
await database.add_history({
|
||||
'url': original_url,
|
||||
'url_type': parsed['type'],
|
||||
'total_count': result.total,
|
||||
'success_count': result.success,
|
||||
'config': json.dumps(config.config, ensure_ascii=False),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def main_async(args):
|
||||
display.show_banner()
|
||||
|
||||
if args.config:
|
||||
config_path = args.config
|
||||
else:
|
||||
config_path = 'config.yml'
|
||||
|
||||
if not Path(config_path).exists():
|
||||
display.print_error(f"Config file not found: {config_path}")
|
||||
return
|
||||
|
||||
config = ConfigLoader(config_path)
|
||||
|
||||
if args.url:
|
||||
urls = args.url if isinstance(args.url, list) else [args.url]
|
||||
for url in urls:
|
||||
if url not in config.get('link', []):
|
||||
config.update(link=config.get('link', []) + [url])
|
||||
|
||||
if args.path:
|
||||
config.update(path=args.path)
|
||||
|
||||
if args.thread:
|
||||
config.update(thread=args.thread)
|
||||
|
||||
if not config.validate():
|
||||
display.print_error("Invalid configuration: missing required fields")
|
||||
return
|
||||
|
||||
cookies = config.get_cookies()
|
||||
cookie_manager = CookieManager()
|
||||
cookie_manager.set_cookies(cookies)
|
||||
|
||||
if not cookie_manager.validate_cookies():
|
||||
display.print_warning("Cookies may be invalid or incomplete")
|
||||
|
||||
database = None
|
||||
if config.get('database'):
|
||||
database = Database()
|
||||
await database.initialize()
|
||||
display.print_success("Database initialized")
|
||||
|
||||
urls = config.get_links()
|
||||
display.print_info(f"Found {len(urls)} URL(s) to process")
|
||||
|
||||
all_results = []
|
||||
|
||||
for i, url in enumerate(urls, 1):
|
||||
display.print_info(f"Processing [{i}/{len(urls)}]: {url}")
|
||||
|
||||
result = await download_url(url, config, cookie_manager, database)
|
||||
if result:
|
||||
all_results.append(result)
|
||||
display.show_result(result)
|
||||
|
||||
if all_results:
|
||||
from core.downloader_base import DownloadResult
|
||||
total_result = DownloadResult()
|
||||
for r in all_results:
|
||||
total_result.total += r.total
|
||||
total_result.success += r.success
|
||||
total_result.failed += r.failed
|
||||
total_result.skipped += r.skipped
|
||||
|
||||
display.print_success("\n=== Overall Summary ===")
|
||||
display.show_result(total_result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Douyin Downloader - 抖音批量下载工具')
|
||||
parser.add_argument('-u', '--url', action='append', help='Download URL(s)')
|
||||
parser.add_argument('-c', '--config', help='Config file path (default: config.yml)')
|
||||
parser.add_argument('-p', '--path', help='Save path')
|
||||
parser.add_argument('-t', '--thread', type=int, help='Thread count')
|
||||
parser.add_argument('--version', action='version', version='1.0.0')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
asyncio.run(main_async(args))
|
||||
except KeyboardInterrupt:
|
||||
display.print_warning("\nDownload interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
display.print_error(f"Fatal error: {e}")
|
||||
logger.exception("Fatal error occurred")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
58
DouYin/dy-downloader/cli/progress_display.py
Normal file
58
DouYin/dy-downloader/cli/progress_display.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class ProgressDisplay:
|
||||
def __init__(self):
|
||||
self.console = console
|
||||
|
||||
def show_banner(self):
|
||||
banner = """
|
||||
╔══════════════════════════════════════════╗
|
||||
║ Douyin Downloader v1.0.0 ║
|
||||
║ 抖音批量下载工具 ║
|
||||
╚══════════════════════════════════════════╝
|
||||
"""
|
||||
self.console.print(banner, style="bold cyan")
|
||||
|
||||
def create_progress(self) -> Progress:
|
||||
return Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console=self.console
|
||||
)
|
||||
|
||||
def show_result(self, result):
|
||||
table = Table(title="Download Summary", show_header=True, header_style="bold magenta")
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Count", justify="right", style="green")
|
||||
|
||||
table.add_row("Total", str(result.total))
|
||||
table.add_row("Success", str(result.success))
|
||||
table.add_row("Failed", str(result.failed))
|
||||
table.add_row("Skipped", str(result.skipped))
|
||||
|
||||
if result.total > 0:
|
||||
success_rate = (result.success / result.total) * 100
|
||||
table.add_row("Success Rate", f"{success_rate:.1f}%")
|
||||
|
||||
self.console.print(table)
|
||||
|
||||
def print_info(self, message: str):
|
||||
self.console.print(f"[blue]ℹ[/blue] {message}")
|
||||
|
||||
def print_success(self, message: str):
|
||||
self.console.print(f"[green]✓[/green] {message}")
|
||||
|
||||
def print_warning(self, message: str):
|
||||
self.console.print(f"[yellow]⚠[/yellow] {message}")
|
||||
|
||||
def print_error(self, message: str):
|
||||
self.console.print(f"[red]✗[/red] {message}")
|
||||
Reference in New Issue
Block a user