diff --git a/README.md b/README.md index aea532d..34da2df 100644 --- a/README.md +++ b/README.md @@ -165,13 +165,10 @@ from StreamingCommunity.Download import TOR_downloader client = TOR_downloader() # Add magnet link -client.add_magnet_link("magnet:?xt=urn:btih:example_hash&dn=example_name") +client.add_magnet_link("magnet:?xt=urn:btih:example_hash&dn=example_name", save_path=".") # Start download client.start_download() - -# Move downloaded files to specific location -client.move_downloaded_files("/downloads/torrents/") ``` See [Torrent example](./Test/Download/TOR.py) for complete usage. diff --git a/StreamingCommunity/Api/Site/1337xx/title.py b/StreamingCommunity/Api/Site/1337xx/title.py index ffd72f7..fa6d4b1 100644 --- a/StreamingCommunity/Api/Site/1337xx/title.py +++ b/StreamingCommunity/Api/Site/1337xx/title.py @@ -58,6 +58,5 @@ def download_title(select_title: MediaItem): # Tor manager manager = TOR_downloader() - manager.add_magnet_link(final_url) - manager.start_download() - manager.move_downloaded_files(mp4_path) \ No newline at end of file + manager.add_magnet_link(final_url, save_path=mp4_path) + manager.start_download() \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/TOR/downloader.py b/StreamingCommunity/Lib/Downloader/TOR/downloader.py index 5004cf9..f63b731 100644 --- a/StreamingCommunity/Lib/Downloader/TOR/downloader.py +++ b/StreamingCommunity/Lib/Downloader/TOR/downloader.py @@ -4,13 +4,15 @@ import os import re import sys import time -import shutil import psutil import logging +from pathlib import Path -# External library +# External libraries from rich.console import Console +from tqdm import tqdm +import qbittorrentapi # Internal utilities @@ -19,19 +21,12 @@ from StreamingCommunity.Util.os import internet_manager from StreamingCommunity.Util.config_json import config_manager, get_use_large_bar -# External libraries -from tqdm import tqdm -import qbittorrentapi +# Configuration +HOST = config_manager.get('QBIT_CONFIG', 'host') +PORT = config_manager.get('QBIT_CONFIG', 'port') +USERNAME = config_manager.get('QBIT_CONFIG', 'user') +PASSWORD = config_manager.get('QBIT_CONFIG', 'pass') - -# Tor config -HOST = config_manager.get_dict('QBIT_CONFIG', 'host') -PORT = config_manager.get_dict('QBIT_CONFIG', 'port') -USERNAME = config_manager.get_dict('QBIT_CONFIG', 'user') -PASSWORD = config_manager.get_dict('QBIT_CONFIG', 'pass') - - -# Variable REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') console = Console() @@ -39,268 +34,443 @@ console = Console() class TOR_downloader: def __init__(self): """ - Initializes the TorrentManager instance. - - Parameters: - - host (str): IP address or hostname of the qBittorrent Web UI. - - port (int): Port of the qBittorrent Web UI. - - username (str): Username for accessing qBittorrent. - - password (str): Password for accessing qBittorrent. + Initializes the TorrentDownloader instance and connects to qBittorrent. """ + self.console = Console() + self.latest_torrent_hash = None + self.output_file = None + self.file_name = None + self.save_path = None + self.torrent_name = None + + self._connect_to_client() + + def _connect_to_client(self): + """ + Establishes connection to qBittorrent client using configuration parameters. + """ + self.console.print(f"[cyan]Connecting to qBittorrent: [green]{HOST}:{PORT}") + try: - console.print(f"[cyan]Connect to: [green]{HOST}:{PORT}") + # Create client with connection settings and timeouts self.qb = qbittorrentapi.Client( host=HOST, port=PORT, username=USERNAME, - password=PASSWORD + password=PASSWORD, + VERIFY_WEBUI_CERTIFICATE=False, + REQUESTS_ARGS={'timeout': REQUEST_TIMEOUT} ) - - except: - logging.error("Start qbittorrent first.") - sys.exit(0) - - self.username = USERNAME - self.password = PASSWORD - self.latest_torrent_hash = None - self.output_file = None - self.file_name = None - - self.login() - - def login(self): - """ - Logs into the qBittorrent Web UI. - """ - try: + + # Test connection and login self.qb.auth_log_in() - self.logged_in = True - logging.info("Successfully logged in to qBittorrent.") - + qb_version = self.qb.app.version + self.console.print(f"[green]Successfully connected to qBittorrent v{qb_version}") + except Exception as e: - logging.error(f"Failed to log in: {str(e)}") - self.logged_in = False - - def delete_magnet(self, torrent_info): + logging.error(f"Unexpected error: {str(e)}") + self.console.print(f"[bold red]Error initializing qBittorrent client: {str(e)}[/bold red]") + sys.exit(1) + + def add_magnet_link(self, magnet_link, save_path=None): """ - Deletes a torrent if it is not downloadable (no seeds/peers). + Adds a magnet link to qBittorrent and retrieves torrent information. - Parameters: - - torrent_info: Object containing torrent information obtained from the qBittorrent API. - """ - if (int(torrent_info.dlspeed) == 0 and - int(torrent_info.num_leechs) == 0 and - int(torrent_info.num_seeds) == 0): + Args: + magnet_link (str): Magnet link to add to qBittorrent + save_path (str, optional): Directory where to save the downloaded files - console.print(f"[bold red]Torrent not downloadable. Removing...[/bold red]") - try: - self.qb.torrents_delete(delete_files=True, torrent_hashes=torrent_info.hash) - except Exception as delete_error: - logging.error(f"Error while removing torrent: {delete_error}") - - self.latest_torrent_hash = None - - def add_magnet_link(self, magnet_link): - """ - Adds a magnet link and retrieves detailed torrent information. - - Arguments: - magnet_link (str): Magnet link to add. - Returns: - dict: Information about the added torrent, or None in case of error. + TorrentDictionary: Information about the added torrent + + Raises: + ValueError: If magnet link is invalid or torrent can't be added """ + # Extract hash from magnet link magnet_hash_match = re.search(r'urn:btih:([0-9a-fA-F]+)', magnet_link) if not magnet_hash_match: - raise ValueError("Magnet link hash not found") + raise ValueError("Invalid magnet link: hash not found") magnet_hash = magnet_hash_match.group(1).lower() - # Extract the torrent name, if available + # Extract torrent name from magnet link if available name_match = re.search(r'dn=([^&]+)', magnet_link) - torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Name not available" + torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Unknown" - # Save the timestamp before adding the torrent + # Record timestamp before adding torrent for identification before_add_time = time.time() - console.print(f"[cyan]Adding magnet link ...") - self.qb.torrents_add(urls=magnet_link) + self.console.print(f"[cyan]Adding magnet link for: [yellow]{torrent_name}") - time.sleep(1) + # Prepare save path + if save_path: + self.console.print(f"[cyan]Setting save location to: [green]{save_path}") + + # Ensure save path exists + os.makedirs(save_path, exist_ok=True) - torrents = self.qb.torrents_info() - matching_torrents = [ - t for t in torrents - if (t.hash.lower() == magnet_hash) or (getattr(t, 'added_on', 0) > before_add_time) - ] + # Add the torrent with save options + add_options = { + "urls": magnet_link, + "use_auto_torrent_management": False, # Don't use automatic management + "is_paused": False, # Start download immediately + "tags": ["StreamingCommunity"] # Add tag for easy identification + } + + # If save_path is provided, add it to options + if save_path: + add_options["save_path"] = save_path + + add_result = self.qb.torrents_add(**add_options) + + if not add_result == "Ok.": + raise ValueError(f"Failed to add torrent: {add_result}") + + # Wait for torrent to be recognized by the client + time.sleep(1.5) + + # Find the newly added torrent + matching_torrents = self._find_torrent(magnet_hash, before_add_time) if not matching_torrents: - raise ValueError("No matching torrent found") + raise ValueError("Torrent was added but couldn't be found in client") torrent_info = matching_torrents[0] - console.print("\n[bold green]Added Torrent Details:[/bold green]") - console.print(f"[yellow]Name:[/yellow] {torrent_info.name or torrent_name}") - console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}") - print() - + # Store relevant information self.latest_torrent_hash = torrent_info.hash self.output_file = torrent_info.content_path self.file_name = torrent_info.name - - # Wait and verify if the download is possible - time.sleep(5) - self.delete_magnet(self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]) + self.save_path = torrent_info.save_path + + # Display torrent information + self._display_torrent_info(torrent_info) + + # Check download viability after a short delay + time.sleep(3) + self._check_torrent_viability() return torrent_info - - def start_download(self): + + def _find_torrent(self, magnet_hash=None, timestamp=None): """ - Starts downloading the added torrent and monitors its progress. - """ - if self.latest_torrent_hash is not None: - try: - - # Custom progress bar for mobile and PC - if get_use_large_bar(): - bar_format = ( - f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " - f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " - f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" - ) - - else: - bar_format = ( - f"{Colors.YELLOW}Proc{Colors.WHITE}: " - f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| " - f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" - ) - - progress_bar = tqdm( - total=100, - ascii='░▒█', - bar_format=bar_format, - unit_scale=True, - unit_divisor=1024, - mininterval=0.05 - ) - - with progress_bar as pbar: - while True: - - torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - self.save_path = torrent_info.save_path - self.torrent_name = torrent_info.name - - progress = torrent_info.progress * 100 - pbar.n = progress - - download_speed = torrent_info.dlspeed - total_size = torrent_info.size - downloaded_size = torrent_info.downloaded - - # Format the downloaded size - downloaded_size_str = internet_manager.format_file_size(downloaded_size) - downloaded_size = downloaded_size_str.split(' ')[0] - - # Safely format the total size - total_size_str = internet_manager.format_file_size(total_size) - total_size_parts = total_size_str.split(' ') - if len(total_size_parts) >= 2: - total_size = total_size_parts[0] - total_size_unit = total_size_parts[1] - else: - total_size = total_size_str - total_size_unit = "" - - # Safely format the average download speed - average_internet_str = internet_manager.format_transfer_speed(download_speed) - average_internet_parts = average_internet_str.split(' ') - if len(average_internet_parts) >= 2: - average_internet = average_internet_parts[0] - average_internet_unit = average_internet_parts[1] - else: - average_internet = average_internet_str - average_internet_unit = "" - - if get_use_large_bar(): - pbar.set_postfix_str( - f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} " - f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}" - ) - else: - pbar.set_postfix_str( - f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size}{Colors.RED} {total_size} " - f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}" - ) - - pbar.refresh() - time.sleep(0.2) - - if int(progress) == 100: - break - - except KeyboardInterrupt: - logging.info("Download process interrupted.") - - def is_file_in_use(self, file_path: str) -> bool: - """ - Checks if a file is being used by any process. + Find a torrent by hash or added timestamp. - Parameters: - - file_path (str): The file path to check. + Args: + magnet_hash (str, optional): Hash of the torrent to find + timestamp (float, optional): Timestamp to compare against torrent added_on time Returns: - - bool: True if the file is in use, False otherwise. + list: List of matching torrent objects """ - for proc in psutil.process_iter(['open_files']): - try: - if any(file_path == f.path for f in proc.info['open_files'] or []): - return True - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - return False - - def move_downloaded_files(self, destination: str): - """ - Moves the downloaded files of the most recent torrent to a new location. + # Get list of all torrents with detailed information + torrents = self.qb.torrents_info() - Parameters: - - destination (str): Destination folder. - - Returns: - - bool: True if the move was successful, False otherwise. + if magnet_hash: + # First try to find by hash (most reliable) + hash_matches = [t for t in torrents if t.hash.lower() == magnet_hash] + if hash_matches: + return hash_matches + + if timestamp: + # Fallback to finding by timestamp (least recently added torrent after timestamp) + time_matches = [t for t in torrents if getattr(t, 'added_on', 0) > timestamp] + if time_matches: + # Sort by added_on to get the most recently added + return sorted(time_matches, key=lambda t: getattr(t, 'added_on', 0), reverse=True) + + # If we're just looking for the latest torrent + if not magnet_hash and not timestamp: + if torrents: + return [sorted(torrents, key=lambda t: getattr(t, 'added_on', 0), reverse=True)[0]] + + return [] + + def _display_torrent_info(self, torrent_info): """ - console.print(f"[cyan]Destination folder: [red]{destination}") + Display detailed information about a torrent. + + Args: + torrent_info: Torrent object from qBittorrent API + """ + self.console.print("\n[bold green]Torrent Details:[/bold green]") + self.console.print(f"[yellow]Name:[/yellow] {torrent_info.name}") + self.console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}") + #self.console.print(f"[yellow]Size:[/yellow] {internet_manager.format_file_size(torrent_info.size)}") + self.console.print(f"[yellow]Save Path:[/yellow] {torrent_info.save_path}") + + # Show additional metadata if available + if hasattr(torrent_info, 'category') and torrent_info.category: + self.console.print(f"[yellow]Category:[/yellow] {torrent_info.category}") + + if hasattr(torrent_info, 'tags') and torrent_info.tags: + self.console.print(f"[yellow]Tags:[/yellow] {torrent_info.tags}") + + # Show connection info + self.console.print(f"[yellow]Seeds:[/yellow] {torrent_info.num_seeds} complete, {torrent_info.num_complete} connected") + self.console.print(f"[yellow]Peers:[/yellow] {torrent_info.num_leechs} incomplete, {torrent_info.num_incomplete} connected") + print() + + def _check_torrent_viability(self): + """ + Check if the torrent is viable for downloading (has seeds/peers). + Removes the torrent if it doesn't appear to be downloadable. + """ + if not self.latest_torrent_hash: + return try: - timeout = 5 - elapsed = 0 + # Get updated torrent info + torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - while self.is_file_in_use(self.output_file) and elapsed < timeout: - time.sleep(1) - elapsed += 1 + # Check if torrent has no activity and no source (seeders or peers) + if (torrent_info.dlspeed == 0 and + torrent_info.num_leechs == 0 and + torrent_info.num_seeds == 0 and + torrent_info.state in ('stalledDL', 'missingFiles', 'error')): + + self.console.print(f"[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]") + self._remove_torrent(self.latest_torrent_hash) + self.latest_torrent_hash = None + return False - if elapsed == timeout: - raise Exception(f"File '{self.output_file}' is in use and could not be moved.") - - os.makedirs(destination, exist_ok=True) - - try: - shutil.move(self.output_file, destination) - except OSError as e: - if e.errno == 17: # Error when moving between different disks - shutil.copy2(self.output_file, destination) - os.remove(self.output_file) - else: - raise - - time.sleep(5) - last_torrent = self.qb.torrents_info()[-1] - self.qb.torrents_delete(delete_files=True, torrent_hashes=last_torrent.hash) return True - + except Exception as e: - print(f"Error moving file: {e}") - return False \ No newline at end of file + logging.error(f"Error checking torrent viability: {str(e)}") + return False + + def _remove_torrent(self, torrent_hash, delete_files=True): + """ + Remove a torrent from qBittorrent. + + Args: + torrent_hash (str): Hash of the torrent to remove + delete_files (bool): Whether to delete associated files + """ + try: + self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash) + self.console.print(f"[yellow]Torrent removed from client[/yellow]") + except Exception as e: + logging.error(f"Error removing torrent: {str(e)}") + + def move_completed_torrent(self, destination): + """ + Move a completed torrent to a new destination using qBittorrent's API + + Args: + destination (str): New destination path + + Returns: + bool: True if successful, False otherwise + """ + if not self.latest_torrent_hash: + self.console.print("[yellow]No active torrent to move[/yellow]") + return False + + try: + # Make sure destination exists + os.makedirs(destination, exist_ok=True) + + # Get current state of the torrent + torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] + + if torrent_info.progress < 1.0: + self.console.print("[yellow]Torrent not yet completed. Cannot move.[/yellow]") + return False + + self.console.print(f"[cyan]Moving torrent to: [green]{destination}") + + # Use qBittorrent API to set location + self.qb.torrents_set_location(location=destination, torrent_hashes=self.latest_torrent_hash) + + # Wait a bit for the move operation to complete + time.sleep(2) + + # Verify move was successful + updated_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] + + if Path(updated_info.save_path) == Path(destination): + self.console.print(f"[bold green]Successfully moved torrent to {destination}[/bold green]") + self.save_path = updated_info.save_path + self.output_file = updated_info.content_path + return True + else: + self.console.print(f"[bold red]Failed to move torrent. Current path: {updated_info.save_path}[/bold red]") + return False + + except Exception as e: + logging.error(f"Error moving torrent: {str(e)}") + self.console.print(f"[bold red]Error moving torrent: {str(e)}[/bold red]") + return False + + def start_download(self): + """ + Start downloading the torrent and monitor its progress with a progress bar. + """ + if not self.latest_torrent_hash: + self.console.print("[yellow]No active torrent to download[/yellow]") + return False + + try: + # Ensure the torrent is started + self.qb.torrents_resume(torrent_hashes=self.latest_torrent_hash) + + # Configure progress bar display format based on device + if get_use_large_bar(): + bar_format = ( + f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " + f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " + f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) + else: + bar_format = ( + f"{Colors.YELLOW}Proc{Colors.WHITE}: " + f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| " + f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" + ) + + # Initialize progress bar + with tqdm( + total=100, + ascii='░▒█', + bar_format=bar_format, + unit_scale=True, + unit_divisor=1024, + mininterval=0.1 + ) as pbar: + + was_downloading = True + stalled_count = 0 + + while True: + + # Get updated torrent information + try: + torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] + except (IndexError, qbittorrentapi.exceptions.NotFound404Error): + self.console.print("[bold red]Torrent no longer exists in client[/bold red]") + return False + + # Store the latest path and name + self.save_path = torrent_info.save_path + self.torrent_name = torrent_info.name + self.output_file = torrent_info.content_path + + # Update progress + progress = torrent_info.progress * 100 + pbar.n = progress + + # Get download statistics + download_speed = torrent_info.dlspeed + upload_speed = torrent_info.upspeed + total_size = torrent_info.size + downloaded_size = torrent_info.downloaded + eta = torrent_info.eta # eta in seconds + + # Format sizes and speeds using the existing functions without modification + downloaded_size_str = internet_manager.format_file_size(downloaded_size) + total_size_str = internet_manager.format_file_size(total_size) + download_speed_str = internet_manager.format_transfer_speed(download_speed) + + # Parse the formatted strings to extract numbers and units + # The format is "X.XX Unit" from the format_file_size and format_transfer_speed functions + dl_parts = downloaded_size_str.split(' ') + dl_size_num = dl_parts[0] if len(dl_parts) > 0 else "0" + dl_size_unit = dl_parts[1] if len(dl_parts) > 1 else "B" + + total_parts = total_size_str.split(' ') + total_size_num = total_parts[0] if len(total_parts) > 0 else "0" + total_size_unit = total_parts[1] if len(total_parts) > 1 else "B" + + speed_parts = download_speed_str.split(' ') + speed_num = speed_parts[0] if len(speed_parts) > 0 else "0" + speed_unit = ' '.join(speed_parts[1:]) if len(speed_parts) > 1 else "B/s" + + # Check if download is active + currently_downloading = download_speed > 0 + + # Handle stalled downloads + if was_downloading and not currently_downloading and progress < 100: + stalled_count += 1 + if stalled_count >= 15: # 3 seconds (15 * 0.2) + pbar.set_description(f"{Colors.RED}Stalled") + else: + stalled_count = 0 + pbar.set_description(f"{Colors.GREEN}Active") + + was_downloading = currently_downloading + + # Update progress bar display with formatted statistics + pbar.set_postfix_str( + f"{Colors.GREEN}{dl_size_num} {Colors.RED}{dl_size_unit} {Colors.WHITE}< " + f"{Colors.GREEN}{total_size_num} {Colors.RED}{total_size_unit}{Colors.WHITE}, " + f"{Colors.CYAN}{speed_num} {Colors.RED}{speed_unit}" + ) + pbar.refresh() + + # Check for completion + if int(progress) == 100: + pbar.n = 100 + pbar.refresh() + break + + # Check torrent state for errors + if torrent_info.state in ('error', 'missingFiles', 'unknown'): + self.console.print(f"[bold red]Error in torrent: {torrent_info.state}[/bold red]") + return False + + time.sleep(0.3) + + self.console.print(f"[bold green]Download complete: {self.torrent_name}[/bold green]") + return True + + except KeyboardInterrupt: + self.console.print("[yellow]Download process interrupted[/yellow]") + return False + + except Exception as e: + logging.error(f"Error monitoring download: {str(e)}") + self.console.print(f"[bold red]Error monitoring download: {str(e)}[/bold red]") + return False + + def is_file_in_use(self, file_path): + """ + Check if a file is currently being used by any process. + + Args: + file_path (str): Path to the file to check + + Returns: + bool: True if file is in use, False otherwise + """ + # Convert to absolute path for consistency + file_path = str(Path(file_path).resolve()) + + try: + for proc in psutil.process_iter(['open_files', 'name']): + try: + proc_info = proc.info + if 'open_files' in proc_info and proc_info['open_files']: + for file_info in proc_info['open_files']: + if file_path == file_info.path: + return True + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + return False + + except Exception as e: + logging.error(f"Error checking if file is in use: {str(e)}") + return False + + def cleanup(self): + """ + Clean up resources and perform final operations before shutting down. + """ + if self.latest_torrent_hash: + self._remove_torrent(self.latest_torrent_hash) + + try: + self.qb.auth_log_out() + except: + pass \ No newline at end of file diff --git a/Test/Download/TOR.py b/Test/Download/TOR.py index dfc4078..050ac07 100644 --- a/Test/Download/TOR.py +++ b/Test/Download/TOR.py @@ -19,9 +19,6 @@ start_message() logger = Logger() manager = TOR_downloader() -magnet_link = """magnet:?xt=urn:btih:d1257567d7a76d2e40734f561fd175769285ed33&dn=Alien.Romulus.2024.1080p.BluRay.x264-FHC.mkv&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftorrentclub.space%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=https%3A%2F%2Ftr.burnabyhighstar.com%3A443%2Fannoun -ce&tr=udp%3A%2F%2Ftracker.monitorit4.me%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.0x.tf%3A6969%2Fannounce&tr=http%3A%2F%2Fbt.okmp3.ru%3A2710%2Fannounce&tr=https%3A%2F%2Ftr.doogh.club%3A443%2Fannounce&tr=https%3A%2F%2Ft.btcland.xyz%3A443%2Fannounce&tr=https%3A%2F%2Ftr.fuckbitcoin.xyz%3A443%2Fannounce&tr=http%3A%2F%2Ftrack -er.files.fm%3A6969%2Fannounce""" -manager.add_magnet_link(magnet_link) -manager.start_download() -manager.move_downloaded_files(".") +magnet_link = """magnet:?xt=urn:btih:0E0CDB5387B4C71C740BD21E8144F3735C3F899E&dn=Krapopolis.S02E14.720p.x265-TiPEX&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.darkness.services%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce""" +manager.add_magnet_link(magnet_link, save_path=os.path.join(src_path, "Video")) +manager.start_download() \ No newline at end of file