Transmission

Установка

Так как достали постоянные падения, решил все поставить из репозитория и о чудо все заработало без глюков

#!/bin/sh sudo apt-get install build-essential automake autoconf libtool pkg-config intltool libcurl4-openssl-dev libglib2.0-dev libevent-dev libminiupnpc-dev libminiupnpc5 libappindicator-dev sudo apt-get install ca-certificates libcurl4-openssl-dev libssl-dev pkg-config build-essential checkinstall echo "Install libevent" if [ ! -d ./libevent ]; then git clone https://github.com/libevent/libevent.git ./libevent cd ./libevent else cd ./libevent && git pull fi echo "[CURRENT FOLDER]" `pwd` ./autogen.sh && CFLAGS="-Os -march=native" ./configure --prefix=/usr && make clean && make && sudo make install cd .. echo "Install Transmission client" if [ ! -d ./Transmission ]; then svn co svn://svn.transmissionbt.com/Transmission/trunk ./Transmission cd ./Transmission else cd ./Transmission && svn up fi echo "[CURRENT FOLDER]" `pwd` ./autogen.sh && ./update-version-h.sh && CFLAGS="-Os -march=native" ./configure --prefix=/usr && make clean && make && sudo make install

Настройка

{ "alt-speed-down": 10, "alt-speed-enabled": false, "alt-speed-time-begin": 600, "alt-speed-time-day": 62, "alt-speed-time-enabled": true, "alt-speed-time-end": 60, "alt-speed-up": 1000, "announce-ip": "", "announce-ip-enabled": false, "anti-brute-force-enabled": false, "anti-brute-force-threshold": 100, "bind-address-ipv4": "0.0.0.0", "bind-address-ipv6": "::", "blocklist-enabled": true, "blocklist-url": "https://github.com/Naunter/BT_BlockLists/raw/master/bt_blocklists.gz", "cache-size-mb": 32, "default-trackers": "", "dht-enabled": true, "download-dir": "/mnt/Torrents/downloads", "download-limit": 100, "download-limit-enabled": false, "download-queue-enabled": true, "download-queue-size": 5, "encryption": 1, "idle-seeding-limit": 30, "idle-seeding-limit-enabled": true, "incomplete-dir": "/mnt/Torrents/incomplete", "incomplete-dir-enabled": true, "lpd-enabled": true, "max-peers-global": 200, "message-level": 5, "peer-congestion-algorithm": "", "peer-id-ttl-hours": 6, "peer-limit-global": 200, "peer-limit-per-torrent": 30, "peer-port": 55142, "peer-port-random-high": 65535, "peer-port-random-low": 49152, "peer-port-random-on-start": false, "peer-socket-tos": "le", "pex-enabled": true, "pidfile": "", "port-forwarding-enabled": true, "preallocation": 1, "preferred_transport": "utp", "prefetch-enabled": true, "proxy_url": "", "queue-stalled-enabled": true, "queue-stalled-minutes": 30, "ratio-limit": 2.0, "ratio-limit-enabled": true, "rename-partial-files": true, "reqq": 2000, "rpc-authentication-required": true, "rpc-bind-address": "0.0.0.0", "rpc-enabled": true, "rpc-host-whitelist": "", "rpc-host-whitelist-enabled": true, "rpc-password": "{87ecf70dcfec2d852c300462512e60147b95d04805Pr9H1d", "rpc-port": 9091, "rpc-socket-mode": "0750", "rpc-url": "/transmission/", "rpc-username": "transmission", "rpc-whitelist": "127.0.0.1,192.168.*.*", "rpc-whitelist-enabled": true, "scrape-paused-torrents-enabled": true, "script-torrent-added-enabled": false, "script-torrent-added-filename": "", "script-torrent-done-enabled": false, "script-torrent-done-filename": "", "script-torrent-done-seeding-enabled": false, "script-torrent-done-seeding-filename": "", "seed-queue-enabled": true, "seed-queue-size": 3, "sequential_download": false, "sleep-per-seconds-during-verify": 100, "speed-limit-down": 307200, "speed-limit-down-enabled": true, "speed-limit-up": 307200, "speed-limit-up-enabled": true, "start-added-torrents": true, "start_paused": false, "tcp-enabled": true, "torrent-added-verify-mode": "fast", "trash-original-torrent-files": false, "umask": "022", "upload-limit": 100, "upload-limit-enabled": 0, "upload-slots-per-torrent": 14, "utp-enabled": true, "watch-dir": "/mnt/Torrents/torrents", "watch-dir-enabled": true, "watch-dir-force-generic": false }

Проверка на наличие ошибок

pip3 freeze > requirements.txt

bencode==1.0 bencodepy==0.9.5
#!/usr/bin/env python3 """ Torrent File Validator with Precise File Moving """ import argparse import hashlib import json import logging import os import shutil import sys import traceback from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from datetime import datetime from enum import Enum, auto from pathlib import Path from typing import Dict, List, Optional, Tuple import bencodepy # Constants MIN_TORRENT_SIZE = 45 # bytes MAX_TORRENT_SIZE = 10 * 1024 * 1024 # 10MB VALID_EXTENSIONS = {'.torrent'} REQUIRED_KEYS = {'announce', 'info'} MAX_PIECE_SIZE = 16 * 1024 * 1024 # 16MB INVALID_FILES_DIR = Path.home() / "invalid_torrents" MAX_FILES_TO_SHOW = 10 # Maximum files to show in preview # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) class Color: RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' BLUE = '\033[94m' CYAN = '\033[96m' BOLD = '\033[1m' RESET = '\033[0m' class IssueLevel(Enum): """Severity levels for validation issues""" ERROR = auto() # Critical problems - makes file invalid WARNING = auto() # Non-critical issues - file remains valid INFO = auto() # Informational messages @dataclass class ValidationIssue: """Represents a single validation issue""" message: str level: IssueLevel exception: Optional[Exception] = None @dataclass class TorrentValidationResult: """Contains complete validation results for a torrent file""" filepath: Path is_valid: bool = True # Default to valid size: int = 0 modified_time: datetime = datetime.min info_hash: Optional[str] = None issues: List[ValidationIssue] = None moved: bool = False def __post_init__(self): """Initialize and validate status""" if self.issues is None: self.issues = [] # Ensure is_valid is False if any ERROR issues exist self._update_validity() def add_issue(self, message: str, level: IssueLevel, exception: Exception = None): """Add an issue and update validity status""" self.issues.append(ValidationIssue(message, level, exception)) self._update_validity() def _update_validity(self): """Update validity status based on issues""" self.is_valid = not any(issue.level == IssueLevel.ERROR for issue in self.issues) def to_dict(self) -> Dict: """Convert result to dictionary for JSON serialization""" return { 'filepath': str(self.filepath), 'is_valid': self.is_valid, 'size': self.size, 'modified_time': self.modified_time.isoformat(), 'info_hash': self.info_hash, 'issues': [ { 'message': issue.message, 'level': issue.level.name, 'exception': str(issue.exception) if issue.exception else None } for issue in self.issues ], 'moved': self.moved } class TorrentValidator: """Main torrent validation logic""" def __init__(self, max_workers: int = None): """Initialize with thread pool""" self.max_workers = max_workers or min(4, (os.cpu_count() or 1) + 2) def validate_file(self, filepath: Path) -> TorrentValidationResult: """Validate a single torrent file""" result = TorrentValidationResult(filepath=filepath) try: # Basic file checks if not filepath.exists(): result.add_issue("File not found", IssueLevel.ERROR) return result if filepath.suffix.lower() not in VALID_EXTENSIONS: result.add_issue(f"Invalid file extension (expected {VALID_EXTENSIONS})", IssueLevel.ERROR) return result # File metadata try: stat = filepath.stat() result.size = stat.st_size result.modified_time = datetime.fromtimestamp(stat.st_mtime) if result.size < MIN_TORRENT_SIZE: result.add_issue(f"File too small ({result.size} bytes)", IssueLevel.ERROR) if result.size > MAX_TORRENT_SIZE: result.add_issue(f"Large file size ({result.size} bytes)", IssueLevel.WARNING) except Exception as e: result.add_issue("Failed to get file stats", IssueLevel.ERROR, e) return result # Read and validate file content with filepath.open("rb") as f: data = f.read() # Validate structure if len(data) < MIN_TORRENT_SIZE: result.add_issue(f"File too small ({len(data)} bytes < {MIN_TORRENT_SIZE} minimum)", IssueLevel.ERROR) if not data.startswith(b"d"): result.add_issue("Invalid structure: should start with 'd'", IssueLevel.ERROR) if not result.is_valid: return result # Decode torrent try: decoded = bencodepy.decode(data) except Exception as e: result.add_issue("Failed to decode torrent file", IssueLevel.ERROR, e) return result # Validate content for key in REQUIRED_KEYS: if key.encode() not in decoded: result.add_issue(f"Missing required field: {key}", IssueLevel.ERROR) info = decoded.get(b'info', {}) if not info: result.add_issue("Empty info dictionary", IssueLevel.ERROR) else: piece_length = info.get(b'piece length', 0) if piece_length > MAX_PIECE_SIZE: result.add_issue(f"Large piece size: {piece_length} bytes", IssueLevel.WARNING) if info.get(b'private', 0) == 1: result.add_issue("Private torrent detected", IssueLevel.WARNING) # Calculate hash try: encoded = bencodepy.encode(info) result.info_hash = hashlib.sha1(encoded).hexdigest() except Exception as e: result.add_issue("Failed to calculate info hash", IssueLevel.ERROR, e) except Exception as e: result.add_issue("Unexpected validation error", IssueLevel.ERROR, e) logger.error(f"Error validating {filepath}: {str(e)}") logger.debug(traceback.format_exc()) return result def validate_directory(self, directory: Path, recursive: bool = False) -> List[TorrentValidationResult]: """Validate all torrent files in a directory""" if not directory.is_dir(): raise NotADirectoryError(f"Not a directory: {directory}") # Find torrent files pattern = "**/*.torrent" if recursive else "*.torrent" torrent_files = { f for f in directory.glob(pattern) if f.is_file() and f.suffix.lower() in VALID_EXTENSIONS } if not torrent_files: logger.warning(f"No torrent files found in {directory}") return [] # Process files in parallel results = [] with ThreadPoolExecutor(max_workers=self.max_workers) as executor: future_to_file = { executor.submit(self.validate_file, f): f for f in torrent_files } for future in as_completed(future_to_file): file = future_to_file[future] try: results.append(future.result()) except Exception as e: logger.error(f"Error processing {file}: {str(e)}") results.append(TorrentValidationResult( filepath=file, is_valid=False, issues=[ValidationIssue("Processing failed", IssueLevel.ERROR, e)] )) return results class OutputFormatter: """Handles formatting of validation results""" @staticmethod def color_text(text: str, color: str) -> str: """Apply ANSI color to text""" return f"{color}{text}{Color.RESET}" @classmethod def format_issue(cls, issue: ValidationIssue) -> str: """Format a single validation issue""" if issue.level == IssueLevel.ERROR: color = Color.RED prefix = "ERROR:" elif issue.level == IssueLevel.WARNING: color = Color.YELLOW prefix = "WARNING:" else: color = Color.BLUE prefix = "INFO:" message = f" {prefix} {issue.message}" if issue.exception: message += f" ({str(issue.exception)})" return cls.color_text(message, color) @classmethod def format_results(cls, results: List[TorrentValidationResult], verbose: bool = False, json_output: bool = False) -> str: if json_output: return json.dumps([r.to_dict() for r in results], indent=2) # Prepare statistics total = len(results) valid = sum(1 for r in results if r.is_valid) invalid = total - valid warnings = sum(1 for r in results if any(i.level == IssueLevel.WARNING for i in r.issues)) # Build output output = [ cls.color_text("\nTORRENT VALIDATION REPORT", Color.CYAN), cls.color_text("=" * 60, Color.CYAN), f"Total files: {total}", cls.color_text(f"Valid files: {valid} (includes {warnings} with warnings)", Color.GREEN), cls.color_text(f"Invalid files: {invalid} (with errors)", Color.RED), cls.color_text("=" * 60, Color.CYAN), ] # Add details if verbose if verbose: for result in results: # Show files with issues or all files in verbose mode if verbose or result.issues or not result.is_valid: has_errors = any(issue.level == IssueLevel.ERROR for issue in result.issues) status = "VALID" if not has_errors else "INVALID" color = Color.GREEN if not has_errors else Color.RED output.append(f"\n[{cls.color_text(status, color)}] {result.filepath}") if result.info_hash: output.append(f"{cls.color_text(' Info Hash:', Color.BLUE)} {result.info_hash}") for issue in sorted(result.issues, key=lambda x: x.level.value, reverse=True): output.append(cls.format_issue(issue)) output.append(cls.color_text("\nValidation complete", Color.CYAN)) return "\n".join(output) def move_invalid_files(results: List[TorrentValidationResult]) -> None: """Move invalid torrent files to ~/invalid_torrents""" invalid_files = [r for r in results if not r.is_valid] if not invalid_files: print(Color.GREEN + "No invalid files to move." + Color.RESET) return # Show preview of files to be moved print(Color.YELLOW + f"\nFound {len(invalid_files)} invalid torrent files (with ERRORS):" + Color.RESET) for i, result in enumerate(invalid_files[:MAX_FILES_TO_SHOW]): print(f" {i+1}. {result.filepath}") # Show only ERROR messages (skip WARNINGs) for issue in result.issues: if issue.level == IssueLevel.ERROR: print(f" - {Color.RED}{issue.message}{Color.RESET}") if len(invalid_files) > MAX_FILES_TO_SHOW: print(f" ... and {len(invalid_files)-MAX_FILES_TO_SHOW} more files") print(f"\nAll these files will be moved to: {Color.CYAN}{INVALID_FILES_DIR}{Color.RESET}") try: response = input("\nDo you want to move them? [y/N]: ").strip().lower() if response != 'y': print(Color.YELLOW + "Operation cancelled." + Color.RESET) return # Create target directory if needed INVALID_FILES_DIR.mkdir(exist_ok=True) moved_count = 0 for result in invalid_files: try: dest = INVALID_FILES_DIR / result.filepath.name # Handle filename conflicts counter = 1 while dest.exists(): stem = result.filepath.stem suffix = result.filepath.suffix dest = INVALID_FILES_DIR / f"{stem}_{counter}{suffix}" counter += 1 shutil.move(str(result.filepath), str(dest)) result.moved = True moved_count += 1 logger.info(f"Moved {result.filepath} to {dest}") except Exception as e: logger.error(f"Failed to move {result.filepath}: {str(e)}") print(Color.GREEN + f"\nSuccessfully moved {moved_count}/{len(invalid_files)} files." + Color.RESET) print(f"Location: {Color.CYAN}{INVALID_FILES_DIR}{Color.RESET}") except KeyboardInterrupt: print(Color.YELLOW + "\nOperation cancelled by user." + Color.RESET) except Exception as e: logger.error(f"Error moving files: {str(e)}") def main(): parser = argparse.ArgumentParser( description="Validate torrent files and optionally move invalid ones", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""Examples: Validate single file: %(prog)s file.torrent -v Validate directory: %(prog)s /path/to/torrents -r Move invalid files: %(prog)s /path/to/torrents -m JSON output: %(prog)s /path/to/torrents -j > results.json """) parser.add_argument("path", type=Path, help="Path to torrent file or directory") parser.add_argument("-r", "--recursive", action="store_true", help="Search directories recursively") parser.add_argument("-j", "--json", action="store_true", help="Output results in JSON format") parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed validation information") parser.add_argument("-w", "--workers", type=int, default=None, help="Number of worker threads") parser.add_argument("-m", "--move-invalid", action="store_true", help="Move invalid torrent files") parser.add_argument("--debug", action="store_true", help="Enable debug logging") args = parser.parse_args() try: # Configure logging if args.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) validator = TorrentValidator(max_workers=args.workers) # Validate files if args.path.is_file(): results = [validator.validate_file(args.path)] else: results = validator.validate_directory(args.path, args.recursive) # Display results print(OutputFormatter.format_results(results, args.verbose, args.json)) # Handle invalid files movement if args.move_invalid: move_invalid_files(results) # Exit with appropriate status code sys.exit(0 if all(r.is_valid for r in results) else 1) except KeyboardInterrupt: logger.error("Operation cancelled by user") sys.exit(130) except Exception as e: logger.error(f"Fatal error: {str(e)}") if args.debug: traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()

Block List

http://john.bitsurge.net/public/biglist.p2p.gz

Клиенты

https://github.com/leonsoft-kras/transmisson-remote-gui