September 27Sep 27 I am using a custom Python script designed to run via the User Scripts plugin (e.g., scheduled once a month at night). It automatically scans your media libraries, detects missing subtitles in specific target languages, and translates them on the fly using a local LibreTranslate API instance.FeaturesDirect File Placement: Saves translated .srt files directly next to the original video/subtitle files, maintaining proper naming conventions (.th.srt, .nld.srt, etc.).Non destructive & Efficient: Automatically ignores already translated or existing files.Automated Batch Processing: Handles large .srt files smoothly without overloading the translation API.PrerequisitesUser Scripts Plugin installed on Unraid.A running LibreTranslate container (or any compatible translation API endpoint).Schedule RecommendationSet the schedule to Custom and enter: 0 1 1 * *(This runs the script automatically at 01:00 AM on the 1st day of every month, ensuring zero performance impact during active hours).CustomizationYou can easily change NL_EXTS or TH_EXTS to any target languages (e.g. Spanish .es.srt, German .de.srt, French .fr.srt) and update the language codes passed to translate_srt_file() accordingly.Hope this helps someone else looking to keep multi language subtitle libraries up to date automatically.How to change Target LanguagesIf you want to use different target languages (e.g., German, Spanish, French), you need to adjust 3 areas in the script:Define File Extensions (Configuration section): Add/modify the language extensions you want to scan for:DE_EXTS = ['.de.srt', '.ger.srt'] Define Output Filenames (process_folder function): Set the desired output filename format for the new target file:target_de_file = f"{base}.de.srt"Pass the Target Language Code (process_folder function): When calling translate_srt_file(), pass the source file, output file path, and the ISO-639-1 language code (e.g., "de", "es", "fr", "nl", "th"):translate_srt_file(has_en, target_de_file, "de")Bug Fix02-10-2026: small bug fix that gives it more speed as it sometimes didn't finish the previous translattion correctly and thought the next file was part of the previos. also added some better (I hope) info how to change the script.The ScriptGo to Settings \User Scripts in Unraid.Click Add New Script and name it (e.g., Auto_Subtitle_Sync).Click the gear icon next to it and select Edit Script.Paste the following code:#!/bin/bash# Description: Scans media folders for missing target subtitles and translates them automatically using LibreTranslate.# Schedule: Custom (0 1 1 * *) - Runs on the 1st of every month at 01:00 AM.python3 -u - << 'EOF'import jsonimport osimport reimport timeimport urllib.errorimport urllib.request# ======================================================================# CONFIGURATION# ======================================================================API_URL = "http://192.168.1.10:5000/translate"BATCH_SIZE = 25TIMEOUT = 180MAX_RETRIES = 3BATCH_DELAY = 0.2# Media folders on UnraidMEDIA_FOLDERS = ["/mnt/user/Animation","/mnt/user/Animation_Shows","/mnt/user/Anime","/mnt/user/Anime_Shows","/mnt/user/Documentary","/mnt/user/Documentary-Shows","/mnt/user/Movies","/mnt/user/TV_Shows"]NL_EXTS = ['.nld.srt', '.nl.srt', '.dut.srt']EN_EXTS = ['.eng.srt', '.en.srt']TH_EXTS = ['.th.srt']# ======================================================================def api_translate(texts, target_lang):payload = {"q": texts,"source": "auto","target": target_lang,"format": "text",}data = json.dumps(payload, ensure_ascii=False).encode("utf-8")request = urllib.request.Request(API_URL,data=data,headers={"Content-Type": "application/json"},method="POST",)last_error = Nonefor attempt in range(1, MAX_RETRIES + 1):try:with urllib.request.urlopen(request, timeout=TIMEOUT) as response:result = json.loads(response.read().decode("utf-8"))translated = result.get("translatedText")if isinstance(translated, str):translated = [translated]if not isinstance(translated, list) or len(translated) != len(texts):raise RuntimeError("Unexpected API response structure")return translatedexcept Exception as exc:last_error = excprint(f" [API ERROR] Attempt {attempt}/{MAX_RETRIES}: {exc}", flush=True)if attempt != MAX_RETRIES:time.sleep(attempt * 2)raise RuntimeError(f"Translation failed: {last_error}")def split_blocks(content):content = content.replace("\r\n", "\n").replace("\r", "\n")return re.split(r"\n\s*\n", content.strip())def parse_block(block):lines = block.splitlines()marker = "--" + chr(62) # '-->'pos = next((i for i, line in enumerate(lines) if marker in line), None)if pos is None or not lines[pos + 1:]:return Nonereturn lines[:pos + 1], "\n".join(lines[pos + 1:])def translate_srt_file(source_path, target_path, target_lang):source_name = os.path.basename(source_path)target_name = os.path.basename(target_path)print(f"\n -> Starting translation: {source_name} -> [{target_lang.upper()}]", flush=True)try:with open(source_path, "r", encoding="utf-8-sig", errors="replace") as handle:raw_content = handle.read()except Exception as e:print(f" [ERROR] Unable to read file: {e}", flush=True)return# Strictly initialize local variables per fileblocks = split_blocks(raw_content)output_blocks = list(blocks)translatable_items = []for idx, block in enumerate(blocks):parsed = parse_block(block)if parsed is not None:header, text = parsedtranslatable_items.append((idx, header, text))total = len(translatable_items)if total == 0:print(" [INFO] No translatable subtitle blocks found.", flush=True)return# Process in batches with a fresh counter per filefor start in range(0, total, BATCH_SIZE):batch = translatable_items[start:start + BATCH_SIZE]texts = [item[2] for item in batch]current_count = min(start + BATCH_SIZE, total)print(f" Progress [{target_name}]: [{current_count}/{total}] blocks...", flush=True)try:translations = api_translate(texts, target_lang)for item, translated in zip(batch, translations):idx, header, _ = itemoutput_blocks[idx] = "\n".join(header) + "\n" + translatedexcept Exception as exc:print(f" [BATCH ERROR]: {exc}", flush=True)if BATCH_DELAY:time.sleep(BATCH_DELAY)# Write to temp file and rename atomicallytemp_file = target_path + ".part"try:with open(temp_file, "w", encoding="utf-8", newline="\n") as handle:handle.write("\n\n".join(output_blocks) + "\n")os.replace(temp_file, target_path)print(f" ✔ [SUCCESS] Created: {target_name}\n", flush=True)except Exception as e:print(f" [SAVE ERROR] Failed to save {target_name}: {e}\n", flush=True)if os.path.exists(temp_file):os.remove(temp_file)def find_existing_sub(base_path, extensions):for ext in extensions:candidate = base_path + extif os.path.exists(candidate):return candidatereturn Nonedef process_folder(directory):print(f"\n==================================================", flush=True)print(f" Scanning directory: {directory}", flush=True)print(f"==================================================", flush=True)for root, _, files in os.walk(directory):base_paths = set()for f in files:if f.lower().endswith('.srt') and not f.startswith('.'):clean = re.sub(r'\.(nld|nl|dut|eng|en|th)\.srt$', '', f, flags=re.IGNORECASE)clean = re.sub(r'\.srt$', '', clean, flags=re.IGNORECASE)base_paths.add(os.path.join(root, clean))for base in sorted(list(base_paths)):has_nl = find_existing_sub(base, NL_EXTS)has_en = find_existing_sub(base, EN_EXTS)has_th = find_existing_sub(base, TH_EXTS)target_th_file = f"{base}.th.srt"target_nl_file = f"{base}.nld.srt"# Case 1: Dutch present, Thai missing -> Translate NL to THif not has_th and has_nl:print(f"\n[MATCH] Dutch present, Thai missing for: {os.path.basename(base)}", flush=True)translate_srt_file(has_nl, target_th_file, "th")# Case 2: Neither Thai nor Dutch present, but English is -> Translate EN to NL & THelif not has_th and not has_nl and has_en:print(f"\n[MATCH] Only English present for: {os.path.basename(base)}", flush=True)translate_srt_file(has_en, target_nl_file, "nl")translate_srt_file(has_en, target_th_file, "th")def main():print("==================================================", flush=True)print(" Automatic Subtitle Sync Started ", flush=True)print("==================================================", flush=True)for folder in MEDIA_FOLDERS:if os.path.exists(folder):process_folder(folder)else:print(f"\n[SKIPPED] Directory does not exist: {folder}", flush=True)print("\n==================================================", flush=True)print(" Scan & Translations Completed Successfully! ", flush=True)print("==================================================", flush=True)if __name__ == "__main__":main()EOF Edited October 2Oct 2 by RichyE small bug fix that and a how to change target language
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.