mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-03-17 16:47:29 +00:00
Compare commits
6 Commits
1.4.0
...
460878777d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
460878777d | ||
|
|
9eb6bdbe12 | ||
|
|
41d203aaba | ||
|
|
0c6909be4e | ||
|
|
ead05d08ac | ||
|
|
8c1f51a431 |
@@ -1,62 +0,0 @@
|
|||||||
# Logs and temporary files
|
|
||||||
|
|
||||||
Logs/
|
|
||||||
logs/
|
|
||||||
temp/
|
|
||||||
\*.log
|
|
||||||
|
|
||||||
# Sensitive files
|
|
||||||
|
|
||||||
key_vault.db
|
|
||||||
unshackle/WVDs/
|
|
||||||
unshackle/PRDs/
|
|
||||||
unshackle/cookies/
|
|
||||||
_.prd
|
|
||||||
_.wvd
|
|
||||||
|
|
||||||
# Cache directories
|
|
||||||
|
|
||||||
unshackle/cache/
|
|
||||||
**pycache**/
|
|
||||||
_.pyc
|
|
||||||
_.pyo
|
|
||||||
\*.pyd
|
|
||||||
.Python
|
|
||||||
|
|
||||||
# Development files
|
|
||||||
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
_.swp
|
|
||||||
_.swo
|
|
||||||
|
|
||||||
# Documentation and plans
|
|
||||||
|
|
||||||
plan/
|
|
||||||
CONTRIBUTING.md
|
|
||||||
CONFIG.md
|
|
||||||
AGENTS.md
|
|
||||||
OLD-CHANGELOG.md
|
|
||||||
cliff.toml
|
|
||||||
|
|
||||||
# Installation scripts
|
|
||||||
|
|
||||||
install.bat
|
|
||||||
|
|
||||||
# Test files
|
|
||||||
|
|
||||||
_test_
|
|
||||||
_Test_
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
.venv/
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
78
Dockerfile
78
Dockerfile
@@ -1,78 +0,0 @@
|
|||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
# Set environment variables to reduce image size
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
UV_CACHE_DIR=/tmp/uv-cache
|
|
||||||
|
|
||||||
# Add container metadata
|
|
||||||
LABEL org.opencontainers.image.description="Docker image for Unshackle with all required dependencies for downloading media content"
|
|
||||||
|
|
||||||
# Install base dependencies
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
wget \
|
|
||||||
gnupg \
|
|
||||||
git \
|
|
||||||
curl \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
pkg-config \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Set up repos for mkvtools and bullseye for ccextractor
|
|
||||||
RUN wget -O /etc/apt/keyrings/gpg-pub-moritzbunkus.gpg https://mkvtoolnix.download/gpg-pub-moritzbunkus.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/gpg-pub-moritzbunkus.gpg] https://mkvtoolnix.download/debian/ bookworm main" >> /etc/apt/sources.list \
|
|
||||||
&& echo "deb-src [signed-by=/etc/apt/keyrings/gpg-pub-moritzbunkus.gpg] https://mkvtoolnix.download/debian/ bookworm main" >> /etc/apt/sources.list \
|
|
||||||
&& echo "deb http://ftp.debian.org/debian bullseye main" >> /etc/apt/sources.list
|
|
||||||
|
|
||||||
# Install all dependencies from apt
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
ffmpeg \
|
|
||||||
ccextractor \
|
|
||||||
mkvtoolnix \
|
|
||||||
aria2 \
|
|
||||||
libmediainfo-dev \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install shaka packager
|
|
||||||
RUN wget https://github.com/shaka-project/shaka-packager/releases/download/v2.6.1/packager-linux-x64 \
|
|
||||||
&& chmod +x packager-linux-x64 \
|
|
||||||
&& mv packager-linux-x64 /usr/local/bin/packager
|
|
||||||
|
|
||||||
# Install N_m3u8DL-RE
|
|
||||||
RUN wget https://github.com/nilaoda/N_m3u8DL-RE/releases/download/v0.3.0-beta/N_m3u8DL-RE_v0.3.0-beta_linux-x64_20241203.tar.gz \
|
|
||||||
&& tar -xzf N_m3u8DL-RE_v0.3.0-beta_linux-x64_20241203.tar.gz \
|
|
||||||
&& mv N_m3u8DL-RE /usr/local/bin/ \
|
|
||||||
&& chmod +x /usr/local/bin/N_m3u8DL-RE \
|
|
||||||
&& rm N_m3u8DL-RE_v0.3.0-beta_linux-x64_20241203.tar.gz
|
|
||||||
|
|
||||||
# Create binaries directory and add symlinks for all required executables
|
|
||||||
RUN mkdir -p /app/binaries && \
|
|
||||||
ln -sf /usr/bin/ffprobe /app/binaries/ffprobe && \
|
|
||||||
ln -sf /usr/bin/ffmpeg /app/binaries/ffmpeg && \
|
|
||||||
ln -sf /usr/bin/mkvmerge /app/binaries/mkvmerge && \
|
|
||||||
ln -sf /usr/local/bin/N_m3u8DL-RE /app/binaries/N_m3u8DL-RE && \
|
|
||||||
ln -sf /usr/local/bin/packager /app/binaries/packager && \
|
|
||||||
ln -sf /usr/local/bin/packager /usr/local/bin/shaka-packager && \
|
|
||||||
ln -sf /usr/local/bin/packager /usr/local/bin/packager-linux-x64
|
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN pip install --no-cache-dir uv
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy dependency files and README (required by pyproject.toml)
|
|
||||||
COPY pyproject.toml uv.lock README.md ./
|
|
||||||
|
|
||||||
# Copy source code first
|
|
||||||
COPY unshackle/ ./unshackle/
|
|
||||||
|
|
||||||
# Install dependencies with uv (including the project itself)
|
|
||||||
RUN uv sync --frozen --no-dev
|
|
||||||
|
|
||||||
# Set entrypoint to allow passing commands directly to unshackle
|
|
||||||
ENTRYPOINT ["uv", "run", "unshackle"]
|
|
||||||
CMD ["-h"]
|
|
||||||
39
README.md
39
README.md
@@ -42,45 +42,6 @@ uv tool install git+https://github.com/unshackle-dl/unshackle.git
|
|||||||
uvx unshackle --help # or just `unshackle` once PATH updated
|
uvx unshackle --help # or just `unshackle` once PATH updated
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Installation
|
|
||||||
|
|
||||||
Run unshackle using our pre-built Docker image from GitHub Container Registry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run with default help command
|
|
||||||
docker run --rm ghcr.io/unshackle-dl/unshackle:latest
|
|
||||||
|
|
||||||
# Check environment dependencies
|
|
||||||
docker run --rm ghcr.io/unshackle-dl/unshackle:latest env check
|
|
||||||
|
|
||||||
# Download content (mount directories for persistent data)
|
|
||||||
docker run --rm \
|
|
||||||
-v "$(pwd)/unshackle/downloads:/app/downloads" \
|
|
||||||
-v "$(pwd)/unshackle/cookies:/app/unshackle/cookies" \
|
|
||||||
-v "$(pwd)/unshackle/services:/app/unshackle/services" \
|
|
||||||
-v "$(pwd)/unshackle/WVDs:/app/unshackle/WVDs" \
|
|
||||||
-v "$(pwd)/unshackle/PRDs:/app/unshackle/PRDs" \
|
|
||||||
-v "$(pwd)/unshackle/unshackle.yaml:/app/unshackle.yaml" \
|
|
||||||
ghcr.io/unshackle-dl/unshackle:latest dl SERVICE_NAME CONTENT_ID
|
|
||||||
|
|
||||||
# Run interactively for configuration
|
|
||||||
docker run --rm -it \
|
|
||||||
-v "$(pwd)/unshackle/cookies:/app/unshackle/cookies" \
|
|
||||||
-v "$(pwd)/unshackle/services:/app/unshackle/services" \
|
|
||||||
-v "$(pwd)/unshackle.yaml:/app/unshackle.yaml" \
|
|
||||||
ghcr.io/unshackle-dl/unshackle:latest cfg
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alternative: Build locally**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone and build your own image
|
|
||||||
git clone https://github.com/unshackle-dl/unshackle.git
|
|
||||||
cd unshackle
|
|
||||||
docker build -t unshackle .
|
|
||||||
docker run --rm unshackle env check
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> After installation, you may need to add the installation path to your PATH environment variable if prompted.
|
> After installation, you may need to add the installation path to your PATH environment variable if prompted.
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class dl:
|
|||||||
"-l",
|
"-l",
|
||||||
"--lang",
|
"--lang",
|
||||||
type=LANGUAGE_RANGE,
|
type=LANGUAGE_RANGE,
|
||||||
default="en",
|
default="orig",
|
||||||
help="Language wanted for Video and Audio. Use 'orig' to select the original language, e.g. 'orig,en' for both original and English.",
|
help="Language wanted for Video and Audio. Use 'orig' to select the original language, e.g. 'orig,en' for both original and English.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -436,6 +436,7 @@ class dl:
|
|||||||
**__: Any,
|
**__: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.tmdb_searched = False
|
self.tmdb_searched = False
|
||||||
|
self.search_source = None
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Check if dovi_tool is available when hybrid mode is requested
|
# Check if dovi_tool is available when hybrid mode is requested
|
||||||
@@ -493,34 +494,34 @@ class dl:
|
|||||||
if self.tmdb_id:
|
if self.tmdb_id:
|
||||||
tmdb_title = tags.get_title(self.tmdb_id, kind)
|
tmdb_title = tags.get_title(self.tmdb_id, kind)
|
||||||
else:
|
else:
|
||||||
self.tmdb_id, tmdb_title = tags.search_tmdb(title.title, title.year, kind)
|
self.tmdb_id, tmdb_title, self.search_source = tags.search_show_info(title.title, title.year, kind)
|
||||||
if not (self.tmdb_id and tmdb_title and tags.fuzzy_match(tmdb_title, title.title)):
|
if not (self.tmdb_id and tmdb_title and tags.fuzzy_match(tmdb_title, title.title)):
|
||||||
self.tmdb_id = None
|
self.tmdb_id = None
|
||||||
if list_ or list_titles:
|
if list_ or list_titles:
|
||||||
if self.tmdb_id:
|
if self.tmdb_id:
|
||||||
console.print(
|
console.print(
|
||||||
Padding(
|
Padding(
|
||||||
f"TMDB -> {tmdb_title or '?'} [bright_black](ID {self.tmdb_id})",
|
f"Search -> {tmdb_title or '?'} [bright_black](ID {self.tmdb_id})",
|
||||||
(0, 5),
|
(0, 5),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.print(Padding("TMDB -> [bright_black]No match found[/]", (0, 5)))
|
console.print(Padding("Search -> [bright_black]No match found[/]", (0, 5)))
|
||||||
self.tmdb_searched = True
|
self.tmdb_searched = True
|
||||||
|
|
||||||
if isinstance(title, Movie) and (list_ or list_titles) and not self.tmdb_id:
|
if isinstance(title, Movie) and (list_ or list_titles) and not self.tmdb_id:
|
||||||
movie_id, movie_title = tags.search_tmdb(title.name, title.year, "movie")
|
movie_id, movie_title, _ = tags.search_show_info(title.name, title.year, "movie")
|
||||||
if movie_id:
|
if movie_id:
|
||||||
console.print(
|
console.print(
|
||||||
Padding(
|
Padding(
|
||||||
f"TMDB -> {movie_title or '?'} [bright_black](ID {movie_id})",
|
f"Search -> {movie_title or '?'} [bright_black](ID {movie_id})",
|
||||||
(0, 5),
|
(0, 5),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.print(Padding("TMDB -> [bright_black]No match found[/]", (0, 5)))
|
console.print(Padding("Search -> [bright_black]No match found[/]", (0, 5)))
|
||||||
|
|
||||||
if self.tmdb_id:
|
if self.tmdb_id and getattr(self, 'search_source', None) != 'simkl':
|
||||||
kind = "tv" if isinstance(title, Episode) else "movie"
|
kind = "tv" if isinstance(title, Episode) else "movie"
|
||||||
tags.external_ids(self.tmdb_id, kind)
|
tags.external_ids(self.tmdb_id, kind)
|
||||||
if self.tmdb_year:
|
if self.tmdb_year:
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class Config:
|
|||||||
|
|
||||||
self.set_terminal_bg: bool = kwargs.get("set_terminal_bg", False)
|
self.set_terminal_bg: bool = kwargs.get("set_terminal_bg", False)
|
||||||
self.tag: str = kwargs.get("tag") or ""
|
self.tag: str = kwargs.get("tag") or ""
|
||||||
|
self.tag_group_name: bool = kwargs.get("tag_group_name", True)
|
||||||
|
self.tag_imdb_tmdb: bool = kwargs.get("tag_imdb_tmdb", True)
|
||||||
self.tmdb_api_key: str = kwargs.get("tmdb_api_key") or ""
|
self.tmdb_api_key: str = kwargs.get("tmdb_api_key") or ""
|
||||||
self.update_checks: bool = kwargs.get("update_checks", True)
|
self.update_checks: bool = kwargs.get("update_checks", True)
|
||||||
self.update_check_interval: int = kwargs.get("update_check_interval", 24)
|
self.update_check_interval: int = kwargs.get("update_check_interval", 24)
|
||||||
|
|||||||
@@ -870,7 +870,18 @@ class Subtitle(Track):
|
|||||||
elif sdh_method == "filter-subs":
|
elif sdh_method == "filter-subs":
|
||||||
# Force use of filter-subs
|
# Force use of filter-subs
|
||||||
sub = Subtitles(self.path)
|
sub = Subtitles(self.path)
|
||||||
sub.filter(rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=True, rm_author=True)
|
try:
|
||||||
|
sub.filter(rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=True, rm_author=True)
|
||||||
|
except ValueError as e:
|
||||||
|
if "too many values to unpack" in str(e):
|
||||||
|
# Retry without name removal if the error is due to multiple colons in time references
|
||||||
|
# This can happen with lines like "at 10:00 and 2:00"
|
||||||
|
sub = Subtitles(self.path)
|
||||||
|
sub.filter(
|
||||||
|
rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=False, rm_author=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
sub.save()
|
sub.save()
|
||||||
return
|
return
|
||||||
elif sdh_method == "auto":
|
elif sdh_method == "auto":
|
||||||
@@ -906,7 +917,18 @@ class Subtitle(Track):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sub = Subtitles(self.path)
|
sub = Subtitles(self.path)
|
||||||
sub.filter(rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=True, rm_author=True)
|
try:
|
||||||
|
sub.filter(rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=True, rm_author=True)
|
||||||
|
except ValueError as e:
|
||||||
|
if "too many values to unpack" in str(e):
|
||||||
|
# Retry without name removal if the error is due to multiple colons in time references
|
||||||
|
# This can happen with lines like "at 10:00 and 2:00"
|
||||||
|
sub = Subtitles(self.path)
|
||||||
|
sub.filter(
|
||||||
|
rm_fonts=True, rm_ast=True, rm_music=True, rm_effects=True, rm_names=False, rm_author=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
sub.save()
|
sub.save()
|
||||||
|
|
||||||
def reverse_rtl(self) -> None:
|
def reverse_rtl(self) -> None:
|
||||||
|
|||||||
@@ -44,6 +44,89 @@ def fuzzy_match(a: str, b: str, threshold: float = 0.8) -> bool:
|
|||||||
return ratio >= threshold
|
return ratio >= threshold
|
||||||
|
|
||||||
|
|
||||||
|
def search_simkl(title: str, year: Optional[int], kind: str) -> Tuple[Optional[dict], Optional[str], Optional[int]]:
|
||||||
|
"""Search Simkl API for show information by filename (no auth required)."""
|
||||||
|
log.debug("Searching Simkl for %r (%s, %s)", title, kind, year)
|
||||||
|
|
||||||
|
# Construct appropriate filename based on type
|
||||||
|
filename = f"{title}"
|
||||||
|
if year:
|
||||||
|
filename = f"{title} {year}"
|
||||||
|
|
||||||
|
if kind == "tv":
|
||||||
|
filename += " S01E01.mkv"
|
||||||
|
else: # movie
|
||||||
|
filename += " 2160p.mkv"
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post("https://api.simkl.com/search/file", json={"file": filename}, headers=HEADERS, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
log.debug("Simkl API response received")
|
||||||
|
|
||||||
|
# Handle case where SIMKL returns empty list (no results)
|
||||||
|
if isinstance(data, list):
|
||||||
|
log.debug("Simkl returned list (no matches) for %r", filename)
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# Handle TV show responses
|
||||||
|
if data.get("type") == "episode" and "show" in data:
|
||||||
|
show_info = data["show"]
|
||||||
|
show_title = show_info.get("title")
|
||||||
|
show_year = show_info.get("year")
|
||||||
|
|
||||||
|
# Verify title matches and year if provided
|
||||||
|
if not fuzzy_match(show_title, title):
|
||||||
|
log.debug("Simkl title mismatch: searched %r, got %r", title, show_title)
|
||||||
|
return None, None, None
|
||||||
|
if year and show_year and abs(year - show_year) > 1: # Allow 1 year difference
|
||||||
|
log.debug("Simkl year mismatch: searched %d, got %d", year, show_year)
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
tmdb_id = show_info.get("ids", {}).get("tmdbtv")
|
||||||
|
if tmdb_id:
|
||||||
|
tmdb_id = int(tmdb_id)
|
||||||
|
log.debug("Simkl -> %s (TMDB ID %s)", show_title, tmdb_id)
|
||||||
|
return data, show_title, tmdb_id
|
||||||
|
|
||||||
|
# Handle movie responses
|
||||||
|
elif data.get("type") == "movie" and "movie" in data:
|
||||||
|
movie_info = data["movie"]
|
||||||
|
movie_title = movie_info.get("title")
|
||||||
|
movie_year = movie_info.get("year")
|
||||||
|
|
||||||
|
# Verify title matches and year if provided
|
||||||
|
if not fuzzy_match(movie_title, title):
|
||||||
|
log.debug("Simkl title mismatch: searched %r, got %r", title, movie_title)
|
||||||
|
return None, None, None
|
||||||
|
if year and movie_year and abs(year - movie_year) > 1: # Allow 1 year difference
|
||||||
|
log.debug("Simkl year mismatch: searched %d, got %d", year, movie_year)
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
ids = movie_info.get("ids", {})
|
||||||
|
tmdb_id = ids.get("tmdb") or ids.get("moviedb")
|
||||||
|
if tmdb_id:
|
||||||
|
tmdb_id = int(tmdb_id)
|
||||||
|
log.debug("Simkl -> %s (TMDB ID %s)", movie_title, tmdb_id)
|
||||||
|
return data, movie_title, tmdb_id
|
||||||
|
|
||||||
|
except (requests.RequestException, ValueError, KeyError) as exc:
|
||||||
|
log.debug("Simkl search failed: %s", exc)
|
||||||
|
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def search_show_info(title: str, year: Optional[int], kind: str) -> Tuple[Optional[int], Optional[str], Optional[str]]:
|
||||||
|
"""Search for show information, trying Simkl first, then TMDB fallback. Returns (tmdb_id, title, source)."""
|
||||||
|
simkl_data, simkl_title, simkl_tmdb_id = search_simkl(title, year, kind)
|
||||||
|
|
||||||
|
if simkl_data and simkl_title and fuzzy_match(simkl_title, title):
|
||||||
|
return simkl_tmdb_id, simkl_title, "simkl"
|
||||||
|
|
||||||
|
tmdb_id, tmdb_title = search_tmdb(title, year, kind)
|
||||||
|
return tmdb_id, tmdb_title, "tmdb"
|
||||||
|
|
||||||
|
|
||||||
def search_tmdb(title: str, year: Optional[int], kind: str) -> Tuple[Optional[int], Optional[str]]:
|
def search_tmdb(title: str, year: Optional[int], kind: str) -> Tuple[Optional[int], Optional[str]]:
|
||||||
api_key = _api_key()
|
api_key = _api_key()
|
||||||
if not api_key:
|
if not api_key:
|
||||||
@@ -202,10 +285,8 @@ def tag_file(path: Path, title: Title, tmdb_id: Optional[int] | None = None) ->
|
|||||||
log.debug("Tagging file %s with title %r", path, title)
|
log.debug("Tagging file %s with title %r", path, title)
|
||||||
standard_tags: dict[str, str] = {}
|
standard_tags: dict[str, str] = {}
|
||||||
custom_tags: dict[str, str] = {}
|
custom_tags: dict[str, str] = {}
|
||||||
# To add custom information to the tags
|
|
||||||
# custom_tags["Text to the left side"] = "Text to the right side"
|
|
||||||
|
|
||||||
if config.tag:
|
if config.tag and config.tag_group_name:
|
||||||
custom_tags["Group"] = config.tag
|
custom_tags["Group"] = config.tag
|
||||||
description = getattr(title, "description", None)
|
description = getattr(title, "description", None)
|
||||||
if description:
|
if description:
|
||||||
@@ -216,12 +297,6 @@ def tag_file(path: Path, title: Title, tmdb_id: Optional[int] | None = None) ->
|
|||||||
description = truncated + "..."
|
description = truncated + "..."
|
||||||
custom_tags["Description"] = description
|
custom_tags["Description"] = description
|
||||||
|
|
||||||
api_key = _api_key()
|
|
||||||
if not api_key:
|
|
||||||
log.debug("No TMDB API key set; applying basic tags only")
|
|
||||||
_apply_tags(path, custom_tags)
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(title, Movie):
|
if isinstance(title, Movie):
|
||||||
kind = "movie"
|
kind = "movie"
|
||||||
name = title.name
|
name = title.name
|
||||||
@@ -234,32 +309,60 @@ def tag_file(path: Path, title: Title, tmdb_id: Optional[int] | None = None) ->
|
|||||||
_apply_tags(path, custom_tags)
|
_apply_tags(path, custom_tags)
|
||||||
return
|
return
|
||||||
|
|
||||||
tmdb_title: Optional[str] = None
|
if config.tag_imdb_tmdb:
|
||||||
if tmdb_id is None:
|
# If tmdb_id is provided (via --tmdb), skip Simkl and use TMDB directly
|
||||||
tmdb_id, tmdb_title = search_tmdb(name, year, kind)
|
if tmdb_id is not None:
|
||||||
log.debug("Search result: %r (ID %s)", tmdb_title, tmdb_id)
|
log.debug("Using provided TMDB ID %s for tags", tmdb_id)
|
||||||
if not tmdb_id or not tmdb_title or not fuzzy_match(tmdb_title, name):
|
else:
|
||||||
log.debug("TMDB search did not match; skipping external ID lookup")
|
# Try Simkl first for automatic lookup
|
||||||
|
simkl_data, simkl_title, simkl_tmdb_id = search_simkl(name, year, kind)
|
||||||
|
|
||||||
|
if simkl_data and simkl_title and fuzzy_match(simkl_title, name):
|
||||||
|
log.debug("Using Simkl data for tags")
|
||||||
|
if simkl_tmdb_id:
|
||||||
|
tmdb_id = simkl_tmdb_id
|
||||||
|
|
||||||
|
show_ids = simkl_data.get("show", {}).get("ids", {})
|
||||||
|
if show_ids.get("imdb"):
|
||||||
|
standard_tags["IMDB"] = f"https://www.imdb.com/title/{show_ids['imdb']}"
|
||||||
|
if show_ids.get("tvdb"):
|
||||||
|
standard_tags["TVDB"] = f"https://thetvdb.com/dereferrer/series/{show_ids['tvdb']}"
|
||||||
|
if show_ids.get("tmdbtv"):
|
||||||
|
standard_tags["TMDB"] = f"https://www.themoviedb.org/tv/{show_ids['tmdbtv']}"
|
||||||
|
|
||||||
|
# Use TMDB API for additional metadata (either from provided ID or Simkl lookup)
|
||||||
|
api_key = _api_key()
|
||||||
|
if not api_key:
|
||||||
|
log.debug("No TMDB API key set; applying basic tags only")
|
||||||
_apply_tags(path, custom_tags)
|
_apply_tags(path, custom_tags)
|
||||||
return
|
return
|
||||||
|
|
||||||
tmdb_url = f"https://www.themoviedb.org/{'movie' if kind == 'movie' else 'tv'}/{tmdb_id}"
|
tmdb_title: Optional[str] = None
|
||||||
standard_tags["TMDB"] = tmdb_url
|
if tmdb_id is None:
|
||||||
try:
|
tmdb_id, tmdb_title = search_tmdb(name, year, kind)
|
||||||
ids = external_ids(tmdb_id, kind)
|
log.debug("TMDB search result: %r (ID %s)", tmdb_title, tmdb_id)
|
||||||
except requests.RequestException as exc:
|
if not tmdb_id or not tmdb_title or not fuzzy_match(tmdb_title, name):
|
||||||
log.debug("Failed to fetch external IDs: %s", exc)
|
log.debug("TMDB search did not match; skipping external ID lookup")
|
||||||
ids = {}
|
_apply_tags(path, custom_tags)
|
||||||
else:
|
return
|
||||||
log.debug("External IDs found: %s", ids)
|
|
||||||
|
|
||||||
imdb_id = ids.get("imdb_id")
|
tmdb_url = f"https://www.themoviedb.org/{'movie' if kind == 'movie' else 'tv'}/{tmdb_id}"
|
||||||
if imdb_id:
|
standard_tags["TMDB"] = tmdb_url
|
||||||
standard_tags["IMDB"] = f"https://www.imdb.com/title/{imdb_id}"
|
try:
|
||||||
tvdb_id = ids.get("tvdb_id")
|
ids = external_ids(tmdb_id, kind)
|
||||||
if tvdb_id:
|
except requests.RequestException as exc:
|
||||||
tvdb_prefix = "movies" if kind == "movie" else "series"
|
log.debug("Failed to fetch external IDs: %s", exc)
|
||||||
standard_tags["TVDB"] = f"https://thetvdb.com/dereferrer/{tvdb_prefix}/{tvdb_id}"
|
ids = {}
|
||||||
|
else:
|
||||||
|
log.debug("External IDs found: %s", ids)
|
||||||
|
|
||||||
|
imdb_id = ids.get("imdb_id")
|
||||||
|
if imdb_id:
|
||||||
|
standard_tags["IMDB"] = f"https://www.imdb.com/title/{imdb_id}"
|
||||||
|
tvdb_id = ids.get("tvdb_id")
|
||||||
|
if tvdb_id:
|
||||||
|
tvdb_prefix = "movies" if kind == "movie" else "series"
|
||||||
|
standard_tags["TVDB"] = f"https://thetvdb.com/dereferrer/{tvdb_prefix}/{tvdb_id}"
|
||||||
|
|
||||||
merged_tags = {
|
merged_tags = {
|
||||||
**custom_tags,
|
**custom_tags,
|
||||||
@@ -269,6 +372,8 @@ def tag_file(path: Path, title: Title, tmdb_id: Optional[int] | None = None) ->
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"search_simkl",
|
||||||
|
"search_show_info",
|
||||||
"search_tmdb",
|
"search_tmdb",
|
||||||
"get_title",
|
"get_title",
|
||||||
"get_year",
|
"get_year",
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
# Group or Username to postfix to the end of all download filenames following a dash
|
# Group or Username to postfix to the end of all download filenames following a dash
|
||||||
tag: user_tag
|
tag: user_tag
|
||||||
|
|
||||||
|
# Enable/disable tagging with group name (default: true)
|
||||||
|
tag_group_name: true
|
||||||
|
|
||||||
|
# Enable/disable tagging with IMDB/TMDB/TVDB details (default: true)
|
||||||
|
tag_imdb_tmdb: true
|
||||||
|
|
||||||
# Set terminal background color (custom option not in CONFIG.md)
|
# Set terminal background color (custom option not in CONFIG.md)
|
||||||
set_terminal_bg: false
|
set_terminal_bg: false
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user