qBittorrent под Linux

Страницы :   Пред.  1, 2, 3 ... 41, 42, 43
Ответить
 

umike

Старожил

Стаж: 19 лет 1 месяц

Сообщений: 64

umike · 31-Авг-25 18:42 (27 дней назад)

Hanabishi писал(а):
88157255
umike писал(а):
88157201Если x% отличаются - это ошибка
При обновлении раздачи некоторые файлы могут поменяться полностью. Никакой ошибки тут нет.
я специально написал x%. Раздачи обновляемые добавлением файлов как правило имеют много файлов и просто по кол-ву больше трёх совпадающих файлов уже можно предположить это апдейт старого (такой функции бы тоже не помешало без удалить-добавить) или новый. Короче, пора навайбкодить встроенный ИИ для решения этой задачи
[Профиль]  [ЛС] 

AndreyKot812

Стаж: 14 лет 5 месяцев

Сообщений: 1


AndreyKot812 · 05-Сен-25 12:39 (спустя 4 дня)

скрипт возвращения на сидирование
( писал для себя ( с помощью ИИ) делюсь)
итак : было скачано несколько террабайт контента контент переименован и перемещен ( Иначе медиа центр plex не хотел нормально индексировать и добавять в библиотеку ( про симлинки знаю но как есть). сами файлы от раздач (torrent) сохранил в отдельном каталоге.
задача сопоставить имеемый контент с файлами torrent и вернуться на сидирование.
Скрипту на вход подается каталог с torrent файлами, каталог с контентом . скрипт парсит каталог с контентом и фалы torrent сопоставляет файлы контента с раздачами по размеру . после чего в отдельном каталоге создает (с сохранением структуры как в оригинальной раздаче переименованные ( какв раздаче) симлинки на файлы контента. ( остается добавить файлы torrent обратно в клиент ( на паузе), указав каталог с сгенерированными симлинками,и запустить принудительную проверку, после проверки контента можно начинать раздавать. (скрипт заточен под сериалы( под одиночные фильмы без отдельного каталога срабатывает через раз, пользуюсь другим но он пока недопилен).
скрипт интерактивный:
- адреса каталогов вводятся в диалоговых окнах миниатюрного файлового менеджера,
- выводится подробная статистика найденного и сопоставленного.
- если по ходу выполнения у скрипта возникают вопросы выбора или потверждения ( при обнаружении дублирования) - спрашивает в командной строке что выбррать
- встроен прогресс-бар.
для работы скрипта надо установить три зависимости
1. Python 3 - скрипт написан для Python 3
2. bencodepy - библиотека для работы с Bencode кодировкой (используется в торрент-файлах)
3. tkinter - графический интерфейс (обычно входит в стандартную поставку Python)
изначально пути в скрипте по умолчанию прописаны мои ( до первого запуска после первого запуска скрипт не найдя мои пути откроет каталоги в домашней директории пользователя а после выбора будет будет по умолчанию открывать каталог введенный в пред идущий раз.
скрипт
скрытый текст
Код:
#!/usr/bin/env python3
import os
import bencodepy
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
import re
import json
import time
# Настройки по умолчанию
DEFAULT_BASE_DIR = "/media/andrey/myssd/Download/qbitlink"
CONFIG_FILE = os.path.expanduser("~/.config/torrent_linker/config.json")
def load_config():
    """Загружает конфигурацию из файла"""
    config = {
        'last_torrent_dir': "/media/andrey/NETDISK/torrents",
        'last_content_dir': "/media/andrey/NETDISK/2.Series",
        'last_output_dir': DEFAULT_BASE_DIR,
        'overwrite_mode': 'ask'  # 'ask', 'always', 'never'
    }
    try:
        os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
        if os.path.exists(CONFIG_FILE):
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                loaded_config = json.load(f)
                config.update(loaded_config)
    except:
        pass
    return config
def save_config(config):
    """Сохраняет конфигурацию в файл"""
    try:
        os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(config, f, ensure_ascii=False, indent=2)
    except:
        pass
def get_torrent_name(torrent_path):
    """Извлекает имя для каталога из торрент-файла"""
    try:
        with open(torrent_path, 'rb') as f:
            data = bencodepy.decode(f.read())
        # Пытаемся найти имя в info
        torrent_name = data[b'info'][b'name'].decode('utf-8')
        # Очищаем имя ТОЛЬКО от недопустимых символов для пути
        # НЕ меняем пробелы - они должны сохраниться!
        clean_name = re.sub(r'[<>:"/\\|?*]', '_', torrent_name)
        clean_name = clean_name.strip()
        return clean_name
    except:
        # Если не удалось извлечь из торрента, используем имя файла
        return Path(torrent_path).stem
def get_torrent_structure(torrent_path):
    """Анализирует структуру файлов в торренте"""
    with open(torrent_path, 'rb') as f:
        data = bencodepy.decode(f.read())
    files = []
    if b'files' in data[b'info']:
        # Многфайловый торрент
        for file_info in data[b'info'][b'files']:
            file_path = Path(*[part.decode('utf-8') for part in file_info[b'path']])
            files.append({
                'path': file_path,
                'size': file_info[b'length'],
                'name': file_path.name
            })
    else:
        # Однофайловый торрент
        file_name = data[b'info'][b'name'].decode('utf-8')
        files.append({
            'path': Path(file_name),
            'size': data[b'info'][b'length'],
            'name': file_name
        })
    return files
def build_size_index(content_dir):
    """Создает индекс файлов по размеру для быстрого поиска"""
    size_index = {}
    content_path = Path(content_dir)
    processed_files = 0
    print("📊 Индексирую файлы по размеру...")
    start_time = time.time()
    for file_path in content_path.rglob('*'):
        if file_path.is_file():
            try:
                file_size = file_path.stat().st_size
                if file_size not in size_index:
                    size_index[file_size] = []
                size_index[file_size].append(file_path)
                processed_files += 1
                # Вывод прогресса каждые 1000 файлов
                if processed_files % 1000 == 0:
                    print(f"📁 Обработано файлов: {processed_files}")
            except (OSError, PermissionError) as e:
                continue
    end_time = time.time()
    print(f"✅ Индексация завершена за {end_time - start_time:.1f} секунд")
    print(f"📊 Проиндексировано файлов: {processed_files}")
    print(f"📈 Уникальных размеров: {len(size_index)}")
    return size_index
def find_matching_file(target_size, size_index):
    """Быстро ищет файл соответствующего размера через индекс"""
    if target_size in size_index and size_index[target_size]:
        return size_index[target_size][0]  # Берем первый подходящий файл
    return None
def ask_overwrite(file_path):
    """Спрашивает пользователя о перезаписи файла"""
    root = tk.Tk()
    root.withdraw()
    root.attributes('-topmost', True)
    result = messagebox.askyesnocancel(
        "Файл уже существует",
        f"Файл {file_path.name} уже существует.\n\n"
        "Перезаписать? (Да - перезаписать, Нет - пропустить, Отмена - отменить все)"
    )
    if result is None:  # Cancel
        return 'cancel'
    elif result:  # Yes
        return 'overwrite'
    else:  # No
        return 'skip'
def create_torrent_structure(torrent_path, size_index, output_dir, overwrite_mode='ask'):
    """Создает структуру симлинков для торрента"""
    torrent_files = get_torrent_structure(torrent_path)
    output_path = Path(output_dir)
    # Создаем основную папку если не существует
    output_path.mkdir(parents=True, exist_ok=True)
    created_links = 0
    missing_files = []
    skipped_files = []
    print(f"🔍 Сопоставляю {len(torrent_files)} файлов...")
    for i, file_info in enumerate(torrent_files, 1):
        # Вывод прогресса
        if i % 10 == 0 or i == len(torrent_files):
            print(f"📋 Обработано {i}/{len(torrent_files)} файлов")
        # Ищем файл подходящего размера через индекс
        source_file = find_matching_file(file_info['size'], size_index)
        if not source_file:
            missing_files.append(file_info['path'])
            continue
        # Создаем целевую структуру каталогов
        # Имена должны ТОЧНО совпадать с торрентом!
        target_path = output_path / file_info['path']
        # Для однофайловых торрентов создаем файл в корне
        if len(torrent_files) == 1:
            target_path = output_path / file_info['path'].name
        target_path.parent.mkdir(parents=True, exist_ok=True)
        # Проверяем существование цели
        if target_path.exists():
            if overwrite_mode == 'ask':
                action = ask_overwrite(target_path)
                if action == 'cancel':
                    return created_links, missing_files, skipped_files, 'cancelled'
                elif action == 'skip':
                    skipped_files.append(file_info['path'])
                    continue
                # Для overwrite продолжаем
            elif overwrite_mode == 'never':
                skipped_files.append(file_info['path'])
                continue
            # Для 'always' продолжаем без вопросов
            # Удаляем существующий файл/симлинк
            if target_path.is_symlink() or target_path.is_file():
                target_path.unlink()
            else:
                print(f"⚠️  Внимание: {target_path} - это не файл/симлинк, пропускаем")
                skipped_files.append(file_info['path'])
                continue
        # Создаем симлинк
        try:
            source_abs = source_file.resolve()
            # Создаем относительный симлинк для переносимости
            relative_source = os.path.relpath(source_abs, target_path.parent)
            os.symlink(relative_source, target_path)
            created_links += 1
            print(f"✅ {source_file.name} -> {file_info['path']}")
        except Exception as e:
            print(f"❌ Ошибка создания симлинка {file_info['path']}: {e}")
            missing_files.append(file_info['path'])
    return created_links, missing_files, skipped_files, 'completed'
def process_single_torrent(torrent_path, size_index, base_dir, config):
    """Обрабатывает один торрент-файл"""
    print(f"\n🔍 Обработка: {Path(torrent_path).name}")
    print("=" * 50)
    # Получаем имя для каталога
    try:
        torrent_name = get_torrent_name(torrent_path)
        print(f"📁 Имя каталога: {torrent_name}")
    except Exception as e:
        print(f"❌ Ошибка чтения торрента: {e}")
        return False
    # Создаем полный путь: базовая_папка/имя_торрента/
    output_dir = os.path.join(base_dir, torrent_name)
    print(f"📁 Полный путь для симлинков: {output_dir}")
    # Создаем папку если не существует
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    # Проверяем, есть ли уже файлы в целевой директории
    existing_files = list(Path(output_dir).rglob('*'))
    if existing_files and config['overwrite_mode'] == 'ask':
        root = tk.Tk()
        root.withdraw()
        overwrite_all = messagebox.askyesno(
            "Папка уже существует",
            f"Папка {output_dir} уже содержит файлы.\n\n"
            "Перезаписать все файлы в этой папке?"
        )
        if not overwrite_all:
            print("⏭️  Пропускаем существующую папку")
            return True
    print("🔍 Анализирую торрент...")
    start_time = time.time()
    try:
        created, missing, skipped, status = create_torrent_structure(
            torrent_path, size_index, output_dir, config['overwrite_mode']
        )
        end_time = time.time()
        print(f"⏱️  Время обработки: {end_time - start_time:.1f} секунд")
        if status == 'cancelled':
            print("❌ Операция отменена пользователем")
            return False
        result_message = f"""
📊 РЕЗУЛЬТАТЫ ДЛЯ: {Path(torrent_path).name}
📁 Папка с симлинками: {output_dir}
✅ Создано симлинков: {created}
❌ Не найдено файлов: {len(missing)}
⏭️  Пропущено файлов: {len(skipped)}
{'⚠️  ВНИМАНИЕ: Не все файлы найдены!' if missing else '🎉 Все файлы успешно обработаны!'}
"""
        if missing:
            result_message += f"\n❌ Не найдены файлы для:\n"
            for missing_file in missing[:3]:
                result_message += f"   - {missing_file}\n"
            if len(missing) > 3:
                result_message += f"   ... и еще {len(missing) - 3} файлов\n"
        if skipped:
            result_message += f"\n⏭️  Пропущены файлы:\n"
            for skipped_file in skipped[:3]:
                result_message += f"   - {skipped_file}\n"
            if len(skipped) > 3:
                result_message += f"   ... и еще {len(skipped) - 3} файлов\n"
        print(result_message)
        # Если ни один файл не был создан и есть пропущенные, не создаем папку
        if created == 0 and missing and not skipped:
            try:
                if not list(Path(output_dir).iterdir()):
                    Path(output_dir).rmdir()
                    print(f"🗑️  Удалена пустая папка: {output_dir}")
            except:
                pass
        return True
    except Exception as e:
        error_msg = f"❌ Ошибка обработки {Path(torrent_path).name}: {e}"
        print(error_msg)
        return False
def batch_process_torrents():
    """Основная функция для пакетной обработки"""
    config = load_config()
    root = tk.Tk()
    root.withdraw()
    root.attributes('-topmost', True)
    print("🎯 ПАКЕТНОЕ СОЗДАНИЕ СТРУКТУРЫ ДЛЯ РАЗДАЧИ")
    print("=" * 60)
    # Выбор папки с торрент-файлами
    torrents_dir = filedialog.askdirectory(
        title="Выберите папку с торрент-файлами",
        initialdir=config['last_torrent_dir']
    )
    if not torrents_dir:
        print("❌ Папка с торрентами не выбрана")
        return
    config['last_torrent_dir'] = torrents_dir
    # Выбор папки с переименованными файлами
    content_dir = filedialog.askdirectory(
        title="Выберите папку с вашими переименованными файлами",
        initialdir=config['last_content_dir']
    )
    if not content_dir:
        print("❌ Папка с файлами не выбрана")
        return
    config['last_content_dir'] = content_dir
    # Выбор базовой папки для результатов
    base_dir = filedialog.askdirectory(
        title="Выберите БАЗОВУЮ папку для создания подкаталогов с симлинками",
        initialdir=config['last_output_dir']
    )
    if not base_dir:
        print("❌ Базовая папка не выбрана")
        return
    config['last_output_dir'] = base_dir
    # Настройка режим перезаписи
    overwrite_option = simpledialog.askstring(
        "Режим перезаписи",
        "Режим перезаписи существующих файлов:\n"
        "ask - спрашивать каждый раз\n"
        "always - всегда перезаписывать\n"
        "never - никогда не перезаписывать\n\n"
        "Введите режим (ask/always/never):",
        initialvalue=config['overwrite_mode']
    )
    if overwrite_option and overwrite_option.lower() in ['ask', 'always', 'never']:
        config['overwrite_mode'] = overwrite_option.lower()
    save_config(config)
    # Построение индекса файлов по размеру (ОДИН РАЗ!)
    size_index = build_size_index(content_dir)
    # Поиск всех торрент-файлов
    torrent_files = list(Path(torrents_dir).glob("*.torrent"))
    if not torrent_files:
        print("❌ В выбранной папке нет .torrent файлов")
        return
    # Сортировка по имени
    torrent_files.sort(key=lambda x: x.name.lower())
    print(f"📁 Найдено торрент-файлов: {len(torrent_files)}")
    print("⏳ Начинаю обработку...")
    successful = 0
    failed = 0
    skipped = 0
    for i, torrent_path in enumerate(torrent_files, 1):
        print(f"\n📋 [{i}/{len(torrent_files)}] ", end="")
        result = process_single_torrent(torrent_path, size_index, base_dir, config)
        if result is None:
            skipped += 1
        elif result:
            successful += 1
        else:
            failed += 1
        # Пауза между обработкой для responsiveness
        root.update()
    # Итоговый отчет
    final_report = f"""
🎯 ИТОГИ ПАКЕТНОЙ ОБРАБОТКИ
✅ Успешно обработано: {successful}
❌ Не удалось обработать: {failed}
⏭️  Пропущено: {skipped}
📊 Всего файлов: {len(torrent_files)}
💡 ИНСТРУКЦИЯ ДЛЯ КАЖДОЙ РАЗДАЧИ:
1. Добавьте в qBittorrent оригинальный торрент
2. Укажите соответствующую папку с симлинками
3. Запустите 'Принудительную проверку'
4. Начните раздачу
"""
    print(final_report)
    messagebox.showinfo("Обработка завершена", final_report)
    # Предлагаем открыть папку с результатами
    open_folder = messagebox.askyesno("Открыть папку", "Хотите открыть папку с результатами?")
    if open_folder:
        os.system(f'xdg-open "{base_dir}"')
if __name__ == "__main__":
    # Создаем базовую папку если не существует
    Path(DEFAULT_BASE_DIR).mkdir(parents=True, exist_ok=True)
    batch_process_torrents()
[Профиль]  [ЛС] 

miken1910

Стаж: 14 лет 5 месяцев

Сообщений: 140

miken1910 · 06-Сен-25 21:30 (спустя 1 день 8 часов)

Добрый день.
Вопрос к знатокам: как перенсти торрент-файлы на другую систему?
Пробовал переносить папку с торрент-файлами - не получилось.
[Профиль]  [ЛС] 

Hanabishi

Старожил

Стаж: 15 лет 5 месяцев

Сообщений: 3053

Hanabishi · 06-Сен-25 21:32 (спустя 1 мин.)

miken1910 писал(а):
88179690Пробовал переносить папку с торрент-файлами - не получилось.
В чем заключается это "не получилось"? Все прекрасно переносится при условии совпадения путей.
[Профиль]  [ЛС] 

miken1910

Стаж: 14 лет 5 месяцев

Сообщений: 140

miken1910 · 06-Сен-25 23:11 (спустя 1 час 39 мин.)

После усновки Linux Mint пишет ошибку: "06.09.2025 23:07 - Не удалось восстановить торрент. Возможно, файлы перемещены, или хранилище недоступно. Торрент: «AutoCAD.2026». Причина: «AutoCAD.2026 fast resume rejected. file_stat(/mnt/Other/TorrentDownloads2/AutoCAD.2026/Autodesk.AutoCAD.2026.1.ru-en.iso): mismatching file size»", хотя файлы лежат на жестком диске. Жесткий диск с NTFS примонтирован к системе. Это со всеми торрент-файлами.
[Профиль]  [ЛС] 

Hanabishi

Старожил

Стаж: 15 лет 5 месяцев

Сообщений: 3053

Hanabishi · 06-Сен-25 23:24 (спустя 12 мин.)

miken1910 писал(а):
88180026mismatching file size
Ну он как бы говорит, что сами файлы не совпадают по размеру.
Версия клиента та же? Если был большой прыжок между версиями, всякое может быть.
Ну попробуйте их перехешировать.
[Профиль]  [ЛС] 

miken1910

Стаж: 14 лет 5 месяцев

Сообщений: 140

miken1910 · 07-Сен-25 12:41 (спустя 13 часов, ред. 07-Сен-25 12:41)

Версия клиента таже. Есть подозрение что при переустановке системы диски были смонтированы по-новой и клиент не может понять что этто за диски и думает что файлы перемещены в другое место. Попробую переместить торрент-файлы на тот же жесткий диск - он типа переместит туда же. Перехэширование долго и муторно будет: Не все раздачи скачаны полностью - есть много скачаных выборочно и сверять все не хочется...
Кажется так и делал в прошлый раз.
[Профиль]  [ЛС] 

FakinTosh

Старожил

Стаж: 17 лет

Сообщений: 2218

FakinTosh · 07-Сен-25 15:14 (спустя 2 часа 32 мин.)

miken1910 писал(а):
88181442при переустановке системы диски были смонтированы по-новой
Точка монтирования должна задаваться руками.
Например, у меня два ссд - / для того что 180GB куда установлена система и /home для 1тб где торренты раздаются. Автоматически это никак не будет создано. Только руками указывать чего мне надо от дисков.
[Профиль]  [ЛС] 

miken1910

Стаж: 14 лет 5 месяцев

Сообщений: 140

miken1910 · 07-Сен-25 16:35 (спустя 1 час 21 мин., ред. 07-Сен-25 16:35)

Как раз при установке системы сам вручную разбиваю диск на разделы и монтирую все сам какждый раздел в ручную, но QBittorrent все равно выдает такую ошибку и уже не первый раз. Диск, где у меня находятски торренты, не трогаю - они уже смонтированы давно.
Потом переношу настройки и торрент-файлы на систему, но все равно когда пытаюсь скачать новую раздачу - все равно нужно указывать в ручную путь сохранения, типа думает что того пути сохранения уже нет.
Сейчас перемонтировал остальные разделы в ручную, удалил настроки и торрент-файлы, скопировал настройки и торрент-файлы и... раздача пошла.
Всем спасибо за помощь. Вопрос снят!
[Профиль]  [ЛС] 

umike

Старожил

Стаж: 19 лет 1 месяц

Сообщений: 64

umike · 08-Сен-25 20:18 (спустя 1 день 3 часа, ред. 08-Сен-25 20:18)

miken1910
mismatching file size - несовпадение размеров, т.е. файл в нужном месте он видит но размер файла не тот.
В линуксе нужны
1) та-же точка монтирования
2) владение или права на чтение-запись пользователю qbtuser или полные rw всем начиная с директории в которой лежат торренты (а возможно и всем рекурсивно, начиная с первой от корня)
Монтировать ntfs в линукс будет медленнее, хоть на ntfs3 драйвере не сильно заметно. И на больших дисках могут быть проблемы. У меня на 8 Тб диске на дебиане после заполнения на ~4Тб вдруг вылезло "ntfs3: sd**: no free space to extend mft" как будто упёрлось в лимит 32битной адресации. Непонятно. Забил, переформатировал в родные ext.
[Профиль]  [ЛС] 
 
Ответить
Loading...
Error