Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слева Предыдущая версия
Следующая версия
Предыдущая версия
network:transmission [2025/04/19 20:37] – [Настройка] mirocownetwork:transmission [2026/01/18 21:23] (текущий) mirocow
Строка 3: Строка 3:
 ===== Установка ===== ===== Установка =====
  
-Так как достали постоянные падения, решил все поставить из репозитория и о чудо все заработало без глюков 
-<code bash> 
-#!/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+<code json> 
-if [ ! -./libevent ]; then +
-        git clone https://github.com/libevent/libevent.git ./libevent +    "alt-speed-down": 10, 
-        cd ./libevent +    "alt-speed-enabled": false, 
-else +    "alt-speed-time-begin": 600, 
-        cd ./libevent && git pull +    "alt-speed-time-day": 62, 
-fi +    "alt-speed-time-enabled": true, 
-echo "[CURRENT FOLDER]`pwd` +    "alt-speed-time-end": 60, 
-./autogen.sh && CFLAGS="-Os -march=native" ./configure --prefix=/usr && make clean && make && sudo make install+    "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 
 +
 +</code>
  
-cd ..+===== Проверка на наличие ошибок =====
  
-echo "Install Transmission client" +pip3 freeze > requirements.txt 
-if [ ! -d ./Transmission ]; then +<code python> 
-        svn co svn://svn.transmissionbt.com/Transmission/trunk ./Transmission +bencode==1.0 
-        cd ./Transmission +bencodepy==0.9.5
-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+
 </code> </code>
-===== Настройка ===== 
  
-<code json> +<code python> 
-{}+#!/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() 
 </code> </code>
 +
 ==== Block List ==== ==== Block List ====
  
Строка 44: Строка 553:
  
   * https://github.com/leonsoft-kras/transmisson-remote-gui   * https://github.com/leonsoft-kras/transmisson-remote-gui
 +
 +==== Ошибки ====
 +
 +=== Q: Не уталяется запись в Transmission Remote GUI ===
 +
 +Из-за особенности работы сервера, запись может быть удалена только при наличии свободного места на диске, где располагаются файлы торента