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

Q: Не уталяется запись в Transmission Remote GUI

Из-за особенности работы сервера, запись может быть удалена только при наличии свободного места на диске, где располагаются файлы торента