Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.
Open
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
9 changes: 9 additions & 0 deletions src/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ async def gather_prep(self, meta: dict[str, Any], mode: str) -> dict[str, Any]:
meta['audio_languages'] = None
meta['subtitle_languages'] = None
meta['aither_trumpable'] = None
meta["adult_media"] = False

folder_id = os.path.basename(meta['path'])
if meta.get('uuid') is None:
Expand Down Expand Up @@ -1246,6 +1247,7 @@ async def gather_prep(self, meta: dict[str, Any], mode: str) -> dict[str, Any]:
unique_genres.append(genre)

meta['combined_genres'] = ', '.join(unique_genres) if unique_genres else ''
meta["adult_media"] = self.check_adult_media(meta)

# return duplicate ids so I don't have to catch every site file
# this has the other advantage of stringing imdb for this object
Expand All @@ -1267,6 +1269,13 @@ async def gather_prep(self, meta: dict[str, Any], mode: str) -> dict[str, Any]:

return meta

def check_adult_media(self, meta) -> bool:
adult_keywords = ["xxx", "erotic", "porn", "adult", "orgy"]
if meta.get("tmdb_adult_media", False):
return True
searchable = ", ".join(part for part in (meta.get("keywords", ""), meta.get("combined_genres", "")) if part)
return any(re.search(rf"(^|,\s*){re.escape(keyword)}(\s*,|$)", searchable, re.IGNORECASE) for keyword in adult_keywords)

async def get_cat(self, _video: str, meta: dict[str, Any]) -> Optional[str]:
if meta.get('manual_category'):
manual_category = meta.get('manual_category')
Expand Down
74 changes: 39 additions & 35 deletions src/tmdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ async def tmdb_other_meta(
tmdb_metadata = {}

# Initialize variables that might not be set in all code paths
adult_media = False
backdrop = ""
cast: list[str] = []
certification = ""
Expand Down Expand Up @@ -1131,6 +1132,8 @@ async def tmdb_other_meta(
tmdb_type = media_data.get('type', 'Scripted')
networks = media_data.get('networks', [])

adult_media = media_data.get("adult", False)

production_companies = media_data.get('production_companies', [])
production_countries = media_data.get('production_countries', [])

Expand Down Expand Up @@ -1317,41 +1320,42 @@ async def tmdb_other_meta(

# Build the metadata dictionary
tmdb_metadata = {
'title': title,
'year': year,
'release_date': release_date,
'first_air_date': first_air_date,
'last_air_date': last_air_date,
'imdb_id': imdb_id,
'tvdb_id': tvdb_id,
'origin_country': origin_country,
'original_language': original_language,
'original_title': original_title,
'keywords': keywords,
'genres': genres,
'genre_ids': genre_ids,
'tmdb_creators': creators,
'tmdb_directors': directors,
'tmdb_cast': cast,
'mal_id': mal_id,
'anime': anime,
'demographic': demographic,
'retrieved_aka': retrieved_aka,
'poster': poster,
'tmdb_poster': poster_path,
'logo': logo_path,
'tmdb_logo': tmdb_logo,
'backdrop': backdrop,
'overview': overview,
'tmdb_type': tmdb_type,
'runtime': runtime,
'youtube': youtube,
'certification': certification,
'production_companies': production_companies,
'production_countries': production_countries,
'networks': networks,
'imdb_mismatch': imdb_mismatch,
'mismatched_imdb_id': mismatched_imdb_id
"tmdb_adult_media": adult_media,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Backfill the new TMDb adult fields on already-populated metadata paths.

Adding tmdb_adult_media here isn't sufficient while set_tmdb_metadata() still skips the TMDb fetch as soon as title/year/genres/overview exist. Those paths never populate tmdb_adult_media or TMDb keywords, so Prep.check_adult_media() can classify adult titles as safe.

Suggested fix
# src/tmdb.py
-        essential_fields = ['title', 'year', 'genres', 'overview']
+        essential_fields = ['title', 'year', 'genres', 'overview', 'keywords', 'tmdb_adult_media']
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tmdb.py` at line 1323, The new tmdb_adult_media field (and TMDb keywords)
won't be backfilled because set_tmdb_metadata() short-circuits when
title/year/genres/overview exist; update set_tmdb_metadata() so it still fetches
TMDb data when tmdb_adult_media or tmdb_keywords are missing (or null) even if
title/year/genres/overview are present, then write those fields into the same
metadata paths (tmdb_adult_media and tmdb_keywords) so Prep.check_adult_media()
can see accurate values; reference set_tmdb_metadata(), tmdb_adult_media, and
tmdb_keywords when making the conditional and write-back changes.

"title": title,
"year": year,
"release_date": release_date,
"first_air_date": first_air_date,
"last_air_date": last_air_date,
"imdb_id": imdb_id,
"tvdb_id": tvdb_id,
"origin_country": origin_country,
"original_language": original_language,
"original_title": original_title,
"keywords": keywords,
"genres": genres,
"genre_ids": genre_ids,
"tmdb_creators": creators,
"tmdb_directors": directors,
"tmdb_cast": cast,
"mal_id": mal_id,
"anime": anime,
"demographic": demographic,
"retrieved_aka": retrieved_aka,
"poster": poster,
"tmdb_poster": poster_path,
"logo": logo_path,
"tmdb_logo": tmdb_logo,
"backdrop": backdrop,
"overview": overview,
"tmdb_type": tmdb_type,
"runtime": runtime,
"youtube": youtube,
"certification": certification,
"production_companies": production_companies,
"production_countries": production_countries,
"networks": networks,
"imdb_mismatch": imdb_mismatch,
"mismatched_imdb_id": mismatched_imdb_id,
}

return tmdb_metadata
Expand Down
5 changes: 1 addition & 4 deletions src/trackers/ANT.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json
import os
import platform
import re
from pathlib import Path
from typing import Any, Union

Expand Down Expand Up @@ -220,9 +219,7 @@ async def upload(self, meta: Meta, _) -> bool:
else:
data.update({'noreleasegroup': 1})

genres = f"{meta.get('keywords', '')} {meta.get('combined_genres', '')}"
adult_keywords = ['xxx', 'erotic', 'porn', 'adult', 'orgy']
if any(re.search(rf'(^|,\s*){re.escape(keyword)}(\s*,|$)', genres, re.IGNORECASE) for keyword in adult_keywords):
if meta.get("adult_media", False):
if not meta['unattended'] or (meta['unattended'] and meta.get('unattended_confirm', False)):
console.print('[bold red]Adult content detected[/bold red]')
if cli_ui.ask_yes_no("Are the screenshots safe?", default=False):
Expand Down
4 changes: 1 addition & 3 deletions src/trackers/AR.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ def __init__(self, config: dict[str, Any]) -> None:
self.banned_groups = []

async def get_type(self, meta: dict[str, Any]) -> str:
genres = f"{meta.get('keywords', '')} {meta.get('combined_genres', '')}"
adult_keywords = ['xxx', 'erotic', 'porn', 'adult', 'orgy']
if (meta['type'] == 'DISC' or meta['type'] == 'REMUX') and meta['source'] == 'Blu-ray':
return "14"

Expand Down Expand Up @@ -89,7 +87,7 @@ async def get_type(self, meta: dict[str, Any]) -> str:
if meta['category'] == "MOVIE":
if meta['sd']:
return '7'
elif any(re.search(rf'(^|,\s*){re.escape(keyword)}(\s*,|$)', genres, re.IGNORECASE) for keyword in adult_keywords):
elif meta.get("adult_media", False):
return '13'
else:
return {
Expand Down
17 changes: 4 additions & 13 deletions src/trackers/BHD.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,19 +357,10 @@ async def search_existing(self, meta: dict[str, Any], _disctype: str) -> list[di
meta['skipping'] = "BHD"
return []

genres = f"{meta.get('keywords', '')} {meta.get('combined_genres', '')}"
adult_keywords = ['xxx', 'erotic', 'porn', 'adult', 'orgy']
if any(re.search(rf'(^|,\s*){re.escape(keyword)}(\s*,|$)', genres, re.IGNORECASE) for keyword in adult_keywords):
if (not meta['unattended'] or (meta['unattended'] and meta.get('unattended_confirm', False))):
console.print('[bold red]Porn/xxx is not allowed at BHD.')
if cli_ui.ask_yes_no("Do you want to upload anyway?", default=False):
pass
else:
meta['skipping'] = "BHD"
return []
else:
meta['skipping'] = "BHD"
return []
common = COMMON(config=self.config)
if not common.check_and_confirm_adult_media_upload(meta, self.tracker):
meta["skipping"] = "BHD"
return []

dupes: list[dict[str, Any]] = []
category = meta['category']
Expand Down
47 changes: 34 additions & 13 deletions src/trackers/COMMON.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from src.exportmi import exportInfo
from src.languages import languages_manager

Meta = dict[str, Any]

class COMMON:
LANGUAGE_EQUIVALENCE_GROUPS: tuple[set[str], ...] = (
Expand Down Expand Up @@ -137,7 +138,7 @@ async def makedirs(self, path: str, exist_ok: bool = True) -> None:

async def create_torrent_for_upload(
self,
meta: dict[str, Any],
meta: Meta,
tracker: str,
source_flag: str,
torrent_filename: str = "BASE",
Expand Down Expand Up @@ -178,7 +179,7 @@ async def create_torrent_for_upload(

async def download_tracker_torrent(
self,
meta: dict[str, Any],
meta: Meta,
tracker: str,
headers: Optional[dict[str, str]] = None,
params: Optional[dict[str, str]] = None,
Expand Down Expand Up @@ -212,7 +213,7 @@ async def download_tracker_torrent(

async def create_torrent_ready_to_seed(
self,
meta: dict[str, Any],
meta: Meta,
tracker: str,
source_flag: str,
new_tracker: Union[str, list[str]],
Expand Down Expand Up @@ -256,7 +257,7 @@ async def create_torrent_ready_to_seed(

return None

async def get_torrent_hash(self, meta: dict[str, Any], tracker: str) -> str:
async def get_torrent_hash(self, meta: Meta, tracker: str) -> str:
torrent_path = f"{meta['base_dir']}/tmp/{meta['uuid']}/[{tracker}].torrent"
async with aiofiles.open(torrent_path, 'rb') as torrent_file:
torrent_content = await torrent_file.read()
Expand All @@ -275,7 +276,7 @@ async def get_torrent_hash(self, meta: dict[str, Any], tracker: str) -> str:
info_hash = hashlib.sha1(info, usedforsecurity=False).hexdigest() # SHA1 required for torrent info hash
return info_hash

async def save_image_links(self, meta: dict[str, Any], image_key: str, image_list: Optional[list[dict[str, str]]]) -> Optional[str]:
async def save_image_links(self, meta: Meta, image_key: str, image_list: Optional[list[dict[str, str]]]) -> Optional[str]:
if image_list is None:
console.print("[yellow]No image links to save.[/yellow]")
return None
Expand Down Expand Up @@ -458,7 +459,7 @@ async def unit3d_distributor_ids(self, distributor: str = "", reverse: bool = Fa

async def prompt_user_for_id_selection(
self,
meta: dict[str, Any],
meta: Meta,
tmdb: Optional[Union[str, int]] = None,
imdb: Optional[Union[str, int]] = None,
tvdb: Optional[Union[str, int]] = None,
Expand Down Expand Up @@ -500,7 +501,7 @@ async def prompt_user_for_confirmation(self, message: str) -> bool:
response = input(f"{message} (Y/n): ").strip().lower()
return bool(response == '' or response == 'y')

async def unit3d_region_distributor(self, meta: dict[str, Any], tracker: str, torrent_url: str, id: str = "") -> None:
async def unit3d_region_distributor(self, meta: Meta, tracker: str, torrent_url: str, id: str = "") -> None:
"""Get region and distributor information from API response"""
raw_api_key = self.config['TRACKERS'][tracker].get('api_key')
api_key = str(raw_api_key).strip() if raw_api_key else ''
Expand Down Expand Up @@ -581,7 +582,7 @@ async def unit3d_torrent_info(
tracker: str,
torrent_url: str,
search_url: str,
meta: dict[str, Any],
meta: Meta,
id: Optional[Union[str, int]] = None,
file_name: Optional[Union[str, list[str]]] = None,
only_id: bool = False,
Expand Down Expand Up @@ -757,7 +758,7 @@ async def parseCookieFile(self, cookiefile: str) -> dict[str, str]:
cookies[lineFields[5]] = lineFields[6]
return cookies

async def ptgen(self, meta: dict[str, Any], ptgen_site: str = "", ptgen_retry: int = 3) -> str:
async def ptgen(self, meta: Meta, ptgen_site: str = "", ptgen_retry: int = 3) -> str:
ptgen_text = ""
url = 'https://ptgen.zhenzhen.workers.dev'
if ptgen_site != '':
Expand Down Expand Up @@ -1074,7 +1075,7 @@ def format_bbcode(self, parsed_mediainfo: dict[str, Any]) -> str:
bbcode_output += "\n"
return bbcode_output

async def get_bdmv_mediainfo(self, meta: dict[str, Any], remove: Optional[list[str]] = None, char_limit: int = 0) -> str:
async def get_bdmv_mediainfo(self, meta: Meta, remove: Optional[list[str]] = None, char_limit: int = 0) -> str:
"""
Generate and sanitize MediaInfo for BDMV discs.

Expand Down Expand Up @@ -1155,7 +1156,7 @@ async def read_and_clean() -> str:

async def check_language_requirements(
self,
meta: dict[str, Any],
meta: Meta,
tracker: str,
languages_to_check: list[str],
check_audio: bool = False,
Expand All @@ -1172,7 +1173,7 @@ async def check_language_requirements(
with subtitles if the primary audio requirement isn't met.

:param meta: Dictionary containing media metadata (audio_languages, subtitle_languages, etc.).
:type meta: dict[str, Any]
:type meta: Meta
:param tracker: Name of the tracker being processed, used for logging/output.
:type tracker: str
:param languages_to_check: A list of language names or codes to search for.
Expand Down Expand Up @@ -1298,7 +1299,7 @@ async def check_language_requirements(
console.print(f"[red]Error checking language requirements: {e}[/red]")
return False

async def save_html_file(self, meta: dict[str, Any], tracker: str, text: str = "", file_name: str = "") -> str:
async def save_html_file(self, meta: Meta, tracker: str, text: str = "", file_name: str = "") -> str:
"""
Save provided text as an HTML file.

Expand All @@ -1313,3 +1314,23 @@ async def save_html_file(self, meta: dict[str, Any], tracker: str, text: str = "
async with aiofiles.open(html_path, "w", encoding="utf-8") as f:
await f.write(text)
return html_path

def check_and_confirm_adult_media_upload(self, meta: Meta, tracker) -> bool:
"""
Check if the media is categorized as adult/pornographic and prompt the user for confirmation before uploading to a non-adult tracker.

:param meta: Metadata dictionary containing category and genre information.
:param tracker: The tracker name for display in the prompt.
:return: True if the user confirms or if the media is not adult, False otherwise.
"""
if meta.get("adult_media", False):
if not meta["unattended"] or (meta["unattended"] and meta.get("unattended_confirm", False)):
console.print(f"[bold red]Pornography is not allowed at {tracker}.[/bold red]")
if cli_ui.ask_yes_no("Do you want to upload anyway?", default=False):
pass
else:
return False
else:
return False

return True
15 changes: 2 additions & 13 deletions src/trackers/IHD.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
import re
from typing import Any, Optional, cast

import cli_ui
import pycountry

from src.console import console
Expand Down Expand Up @@ -210,16 +208,7 @@ async def get_additional_checks(self, meta: Meta) -> bool:
console.print(f'[bold red]{self.tracker} requires at least one English audio or subtitle track or an original language audio track.')
should_continue = False

genres = f"{meta.get('keywords', '')} {meta.get('combined_genres', '')}"
adult_keywords = ['xxx', 'erotic', 'porn', 'adult', 'orgy']
if any(re.search(rf'(^|,\s*){re.escape(keyword)}(\s*,|$)', genres, re.IGNORECASE) for keyword in adult_keywords):
if (not meta['unattended'] or (meta['unattended'] and meta.get('unattended_confirm', False))):
console.print(f'[bold red]Pornographic content is not allowed at {self.tracker}, unless it follows strict rules.')
yes = cli_ui.ask_yes_no(f'Do you have permission to upload this torrent to {self.tracker}?', default=False)
should_continue = bool(yes)
else:
if not meta['unattended'] or meta['debug']:
console.print('[bold red]Pornographic content is not allowed at IHD, unless it follows strict rules.')
should_continue = False
if not self.common.check_and_confirm_adult_media_upload(meta, self.tracker):
return False

return should_continue
14 changes: 2 additions & 12 deletions src/trackers/LUME.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Upload Assistant © 2025 Audionut & wastaken7 — Licensed under UAPL v1.0
import re
from typing import Any

import cli_ui
Expand Down Expand Up @@ -54,16 +53,7 @@ async def get_additional_checks(self, meta: Meta) -> bool:
console.print(f"[bold red]No encoding settings in mediainfo, skipping {self.tracker} upload.[/bold red]")
return False

genres = f"{meta.get('keywords', '')} {meta.get('combined_genres', '')}"
adult_keywords = ['xxx', 'erotic', 'porn', 'adult', 'orgy']
if any(re.search(rf'(^|,\s*){re.escape(keyword)}(\s*,|$)', genres, re.IGNORECASE) for keyword in adult_keywords):
if not meta['unattended'] or (meta['unattended'] and meta.get('unattended_confirm', False)):
console.print(f"[bold red]Pornography is not allowed at {self.tracker}.[/bold red]")
if cli_ui.ask_yes_no("Do you want to upload anyway?", default=False):
pass
else:
return False
else:
return False
if not self.common.check_and_confirm_adult_media_upload(meta, self.tracker):
return False

return should_continue
Loading
Loading