Различия

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

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

Предыдущая версия справа и слева Предыдущая версия
Следующая версия
Предыдущая версия
network:transmission [2017/02/12 03:35] – [Клиенты] mirocownetwork:transmission [2025/05/06 21:39] (текущий) mirocow
Строка 34: Строка 34:
 ===== Настройка ===== ===== Настройка =====
  
 +<code json>
 +{
 +    "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
 +}
 +</code>
 +
 +===== Проверка на наличие ошибок =====
 +
 +pip3 freeze > requirements.txt
 +<code python>
 +bencode==1.0
 +bencodepy==0.9.5
 +</code>
 +
 +<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>
 +
 +==== Block List ====
 +
 +<code>http://john.bitsurge.net/public/biglist.p2p.gz</code>
  
 ==== Клиенты ==== ==== Клиенты ====
  
   * https://github.com/leonsoft-kras/transmisson-remote-gui   * https://github.com/leonsoft-kras/transmisson-remote-gui
 +
 +==== Ошибки ====
 +
 +=== Q: Не уталяется запись в Transmission Remote GUI ===
 +
 +Из-за особенности работы сервера, запись может быть удалена только при наличии свободного места на диске, где располагаются файлы торента
 +