Это старая версия документа!


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()
http://john.bitsurge.net/public/biglist.p2p.gz