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 and Cleaner This script validates torrent files, checks their structure and content, and provides options to move invalid files to a separate directory. Features: - Validates both single torrent files and directories of torrents - Recursive directory scanning - Parallel processing for performance - Detailed validation reporting - Interactive cleanup of invalid files - JSON output support - Colorized console output Exit codes: 0 - All files valid 1 - One or more invalid files found 130 - Operation cancelled by user (Ctrl+C) """ 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
Клиенты
Ошибки
Q: Не уталяется запись в Transmission Remote GUI
Из-за особенности работы сервера, запись может быть удалена только при наличии свободного места на диске, где располагаются файлы торента