Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.git
.gitignore
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.pytest_cache
tests
Docker
docker-data
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ unti3dupManyx/

# Dossier de configuration local (normalement dans ~/)
Unit3Dup_config/
docker-data/
config/
docker-compose.override.yml

# Config personnelle (ne jamais committer)
*.json
!pyproject.toml
.env
.env.*
*.secret
*.secrets
*.key
*.pem
*.env

# Base de données locale
*.db
Expand All @@ -35,4 +45,4 @@ Unit3Dup_config/

# IDE
.idea/
.vscode/
.vscode/
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
UNIT3DUP_CONFIG_ROOT=/config \
HOME=/tmp/unit3dup

WORKDIR /app

RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
ffmpeg \
mediainfo \
libmediainfo0v5 \
poppler-utils \
p7zip-full \
unrar-free && \
rm -rf /var/lib/apt/lists/*

COPY requirements.txt pyproject.toml README.md ./
COPY common ./common
COPY unit3dup ./unit3dup
COPY view ./view

RUN pip install --upgrade pip && \
pip install .

RUN mkdir -p /config /watch /done /data "$HOME" && \
chmod 777 "$HOME"

ENTRYPOINT ["unit3dup"]
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,103 @@ pip install -e .
## Projet original

Ce fork est basé sur [Unit3Dup](https://github.com/31December99/Unit3Dup) — licence MIT.

---

## Docker / NAS

Une stack Docker prête à l'emploi est disponible à la racine du dépôt avec :

- `Dockerfile`
- `docker-compose.yml`
- `.dockerignore`

Le conteneur utilise ce fork localement et stocke sa configuration dans `/config` via la variable d'environnement `UNIT3DUP_CONFIG_ROOT`.

### Volumes par défaut

Le `docker-compose.yml` monte ces dossiers :

- `./docker-data/config` -> `/config`
- `./docker-data/watch` -> `/watch`
- `./docker-data/done` -> `/done`
- `./docker-data/media` -> `/data`

Sur un NAS, remplace de préférence ces chemins par tes partages absolus, par exemple :

```yaml
volumes:
- /volume1/docker/unit3dup/config:/config
- /volume1/torrents/watch:/watch
- /volume1/torrents/done:/done
- /volume1/media:/data
```

### 1. Construire l'image

```bash
docker compose build
```

### 2. Générer la configuration initiale

Lance une première fois l'outil pour créer `/config/Unit3Dbot.json` :

```bash
docker compose run --rm unit3dup --help
```

### 3. Éditer la configuration

Modifie ensuite `Unit3Dbot.json` dans ton dossier `config` et adapte au minimum :

```json
"WATCHER_PATH": "/watch",
"WATCHER_DESTINATION_PATH": "/done"
```

Renseigne aussi :

- `Gemini_URL`
- `Gemini_APIKEY`
- `Gemini_PID`
- `TMDB_APIKEY`
- `IMGBB_KEY`

Si tu utilises un client torrent externe sur le NAS, pense également à corriger :

- `QBIT_HOST` / `QBIT_PORT`
- ou `TRASM_HOST` / `TRASM_PORT`
- ou `RTORR_HOST` / `RTORR_PORT`

### 4. Lancer le watcher

```bash
docker compose up -d
```

Le service démarre avec la commande `-watcher`.

### 5. Lancer un upload manuel

Pour envoyer un fichier déjà présent dans le volume `/data` :

```bash
docker compose run --rm unit3dup -u /data/mon_fichier.mkv
```

Pour scanner un dossier :

```bash
docker compose run --rm unit3dup -scan /data/mon_dossier
```

### Permissions NAS

Le compose utilise :

```yaml
user: "${PUID:-1000}:${PGID:-1000}"
```

Adapte `PUID` et `PGID` à l'utilisateur de ton NAS si besoin pour éviter les problèmes d'accès sur les partages.
7 changes: 7 additions & 0 deletions common/external_services/sessions/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import json
import os

import httpx
import diskcache as dc
Expand All @@ -15,6 +16,12 @@ class MyHttp:
"""Class to handle HTTP requests"""

def __init__(self, headers: dict, cache_dir: str = "http_cache"):
if not os.path.isabs(cache_dir):
cache_root = os.getenv("UNIT3DUP_HTTP_CACHE_DIR") or os.path.join(
os.path.expanduser("~"), cache_dir
)
cache_dir = cache_root

self.session = httpx.Client(
timeout=httpx.Timeout(30), headers=headers, verify=False
)
Expand Down
39 changes: 21 additions & 18 deletions common/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,26 @@
config_file = "Unit3Dbot.json"
version = "0.8.21"

if os.name == "nt":
PW_TORRENT_ARCHIVE_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "pw_torrent_archive"
PW_DOWNLOAD_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "pw_download"
WATCHER_DESTINATION_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "watcher_destination_path"
WATCHER_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "watcher_path"
CACHE_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "cache_path"
TORRENT_ARCHIVE_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / "torrent_archive_path"
DEFAULT_JSON_PATH: Path = Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config" / f"{config_file}"

else:
PW_TORRENT_ARCHIVE_PATH: Path = Path.home() / "Unit3Dup_config" / "pw_torrent_archive"
PW_DOWNLOAD_PATH: Path = Path.home() / "Unit3Dup_config" / "pw_download"
WATCHER_DESTINATION_PATH: Path = Path.home() / "Unit3Dup_config" / "watcher_destination_path"
WATCHER_PATH: Path = Path.home() / "Unit3Dup_config" / "watcher_path"
CACHE_PATH: Path = Path.home() / "Unit3Dup_config" / "cache_path"
TORRENT_ARCHIVE_PATH: Path = Path.home() / "Unit3Dup_config" / "torrent_archive_path"
DEFAULT_JSON_PATH: Path = Path.home() / "Unit3Dup_config" / f"{config_file}"
def get_config_root() -> Path:
"""Allow container/runtime overrides while preserving historical defaults."""
config_root_override = os.getenv("UNIT3DUP_CONFIG_ROOT")
if config_root_override:
return Path(config_root_override).expanduser()

if os.name == "nt":
return Path(os.getenv("LOCALAPPDATA", ".")) / "Unit3Dup_config"

return Path.home() / "Unit3Dup_config"


CONFIG_ROOT: Path = get_config_root()
PW_TORRENT_ARCHIVE_PATH: Path = CONFIG_ROOT / "pw_torrent_archive"
PW_DOWNLOAD_PATH: Path = CONFIG_ROOT / "pw_download"
WATCHER_DESTINATION_PATH: Path = CONFIG_ROOT / "watcher_destination_path"
WATCHER_PATH: Path = CONFIG_ROOT / "watcher_path"
CACHE_PATH: Path = CONFIG_ROOT / "cache_path"
TORRENT_ARCHIVE_PATH: Path = CONFIG_ROOT / "torrent_archive_path"
DEFAULT_JSON_PATH: Path = CONFIG_ROOT / f"{config_file}"


def get_default_path(field: str)-> str:
Expand Down Expand Up @@ -898,4 +901,4 @@ def aim(self, line: int, col: int):
print(f"{Ccolors.WARNING} {cursor}{Ccolors.ENDC}")
print(f"{line_context2}")
else:
print("Line number is out of range !")
print("Line number is out of range !")
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
unit3dup:
build:
context: .
container_name: unit3dup
restart: unless-stopped
user: "${PUID:-1000}:${PGID:-1000}"
environment:
TZ: "${TZ:-Europe/Paris}"
UNIT3DUP_CONFIG_ROOT: /config
HOME: /tmp/unit3dup
volumes:
- ./docker-data/config:/config
- ./docker-data/watch:/watch
- ./docker-data/done:/done
- ./docker-data/media:/data
command: ["-watcher"]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dynamic = ["dependencies"]
name = "Unit3Dup"
version = "0.8.21"
description = "An uploader for the Unit3D torrent tracker"
readme = "README.rst"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"

Expand Down
50 changes: 47 additions & 3 deletions unit3dup/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
import argparse
import os
import shutil
import time

from unit3dup.media import Media
Expand Down Expand Up @@ -100,10 +101,15 @@ def run(self) -> bool:
# We want to reseed
if self.cli.reseed:
torrent_manager.reseed(trackers_name_list=self.trackers_name_list)
return True
else:
# otherwise run the torrents creations and the upload process
torrent_manager.run(trackers_name_list=self.trackers_name_list)
return True
upload_results = torrent_manager.run(trackers_name_list=self.trackers_name_list)

if self.cli.noup:
return False

return self._uploads_succeeded(upload_results)


def watcher(self, duration: int, watcher_path: str, destination_path: str)-> bool:
Expand Down Expand Up @@ -160,6 +166,15 @@ def watcher(self, duration: int, watcher_path: str, destination_path: str)-> bo
custom_console.bot_warning_log(
f"[Watcher] Upload failed/skipped, leaving in place -> {src}"
)
continue

moved_to = self._move_to_destination(src=src, done_root=done_root)
if moved_to:
custom_console.bot_log(f"[Watcher] Moved to destination -> {moved_to}")
else:
custom_console.bot_warning_log(
f"[Watcher] Upload succeeded but move failed, leaving in place -> {src}"
)

# Nettoyer les fichiers .nfo isolés dans le dossier watcher après avoir traité tous les fichiers du cycle
self._cleanup_orphaned_nfo_files(watcher_path)
Expand All @@ -181,6 +196,35 @@ def watcher(self, duration: int, watcher_path: str, destination_path: str)-> bo
custom_console.bot_log("Exiting...")
return True

@staticmethod
def _uploads_succeeded(results) -> bool:
if not results:
return False

return all(result.tracker_response and not result.tracker_message for result in results)

@staticmethod
def _next_available_destination(target: Path) -> Path:
if not target.exists():
return target

counter = 1
while True:
candidate = target.with_name(f"{target.name}_{counter}")
if not candidate.exists():
return candidate
counter += 1

def _move_to_destination(self, src: Path, done_root: Path) -> Path | None:
target = self._next_available_destination(done_root / src.name)

try:
shutil.move(str(src), str(target))
return target
except Exception as exc:
custom_console.bot_warning_log(f"[Watcher] Failed to move '{src}' -> '{target}': {exc}")
return None

def _cleanup_orphaned_nfo_files(self, watcher_path: str) -> None:
"""
Supprime uniquement les fichiers .nfo isolés à la RACINE du dossier watcher.
Expand Down Expand Up @@ -272,4 +316,4 @@ def ftp(self)-> None:
exit(1)

self.run()
return None
return None
Loading