From e200933337568601a69d694b92507cbe44dd6abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Limpinho?= <53994778+TomasLimpinho@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:03:00 +0100 Subject: [PATCH] lingarr correction --- roles/bazarr/files/bazarr-deployment.yaml | 11 +- roles/bazarr/files/lingarr-configmap.yaml | 207 ++++++++++++++++++ roles/bazarr/files/teste.yml | Bin 0 -> 13014 bytes roles/lingarr/files/lingarr-configmap.yaml | 24 ++ roles/lingarr/files/lingarr-deployment.yaml | 42 +++- roles/lingarr/files/lingarr-secret.yaml | 10 + roles/lingarr/tasks/main.yml | 24 +- roles/soularr/tasks/main.yml | 14 +- roles/soulseek/files/soulseek-deployment.yaml | 4 +- 9 files changed, 314 insertions(+), 22 deletions(-) create mode 100644 roles/bazarr/files/lingarr-configmap.yaml create mode 100644 roles/bazarr/files/teste.yml create mode 100644 roles/lingarr/files/lingarr-configmap.yaml create mode 100644 roles/lingarr/files/lingarr-secret.yaml diff --git a/roles/bazarr/files/bazarr-deployment.yaml b/roles/bazarr/files/bazarr-deployment.yaml index 72f8de8..256f958 100644 --- a/roles/bazarr/files/bazarr-deployment.yaml +++ b/roles/bazarr/files/bazarr-deployment.yaml @@ -17,7 +17,7 @@ spec: - name: regcred containers: - name: bazarr - image: lscr.io/linuxserver/bazarr:1.5.3 + image: lscr.io/linuxserver/bazarr:1.5.6 securityContext: capabilities: add: @@ -26,6 +26,8 @@ spec: - containerPort: 6767 name: webui env: + - name: BAZARR_DEBUG + value: "true" - name: PUID value: "1013" - name: PGID @@ -58,6 +60,10 @@ spec: mountPath: /config - name: media mountPath: /media + - name: lingarrpy + mountPath: /app/bazarr/bin/bazarr/subtitles/tools/translate/services/lingarr_translator.py + subPath: lingarr_translator.py + readOnly: true volumes: - name: config persistentVolumeClaim: @@ -65,6 +71,9 @@ spec: - name: media persistentVolumeClaim: claimName: bazarr-media-pvc + - name: lingarrpy + configMap: + name: bazarr-lingarrpy-configmap diff --git a/roles/bazarr/files/lingarr-configmap.yaml b/roles/bazarr/files/lingarr-configmap.yaml new file mode 100644 index 0000000..d4e09d5 --- /dev/null +++ b/roles/bazarr/files/lingarr-configmap.yaml @@ -0,0 +1,207 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bazarr-lingarrpy-configmap + namespace: stack-arr +data: + lingarr_translator.py: | + # coding=utf-8 + + import logging + import pysubs2 + import requests + + from retry.api import retry + from deep_translator.exceptions import TooManyRequests, RequestError + + from app.config import settings + from app.database import TableShows, TableEpisodes, TableMovies, database, select + from app.jobs_queue import jobs_queue + from languages.custom_lang import CustomLanguage + from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3 + from radarr.history import history_log_movie + from sonarr.history import history_log + from subtitles.processing import ProcessSubtitlesResult + from utilities.path_mappings import path_mappings + + from ..core.translator_utils import add_translator_info, create_process_result, get_title + + logger = logging.getLogger(__name__) + + + class LingarrTranslatorService: + def __init__(self, source_srt_file, dest_srt_file, lang_obj, to_lang, from_lang, media_type, + video_path, orig_to_lang, forced, hi, sonarr_series_id, sonarr_episode_id, + radarr_id): + self.source_srt_file = source_srt_file + self.dest_srt_file = dest_srt_file + self.lang_obj = lang_obj + self.to_lang = to_lang + self.from_lang = from_lang + self.media_type = media_type + self.video_path = video_path + self.orig_to_lang = orig_to_lang + self.forced = forced + self.hi = hi + self.sonarr_series_id = sonarr_series_id + self.sonarr_episode_id = sonarr_episode_id + self.radarr_id = radarr_id + self.language_code_convert_dict = { + 'zh': 'zh-CN', + 'zt': 'zh-TW', + 'pb': 'pt-BR', + } + + def translate(self, job_id=None): + try: + jobs_queue.update_job_progress(job_id=job_id, progress_max=1, progress_message=self.source_srt_file) + + subs = pysubs2.load(self.source_srt_file, encoding='utf-8') + lines_list = [x.plaintext for x in subs] + lines_list_len = len(lines_list) + + if lines_list_len == 0: + logger.debug('No lines to translate in subtitle file') + return self.dest_srt_file + + logger.debug(f'Starting translation for {self.source_srt_file}') + translated_lines = self._translate_content(lines_list, job_id=job_id) + + if translated_lines is None: + logger.error(f'Translation failed for {self.source_srt_file}') + jobs_queue.update_job_progress(job_id=job_id, + progress_message=f'Translation failed for {self.source_srt_file}') + return False + + logger.debug(f'BAZARR saving Lingarr translated subtitles to {self.dest_srt_file}') + translation_map = {} + for item in translated_lines: + if isinstance(item, dict) and 'position' in item and 'line' in item: + translation_map[item['position']] = item['line'] + + for i, line in enumerate(subs): + if i in translation_map and translation_map[i]: + line.text = translation_map[i] + + try: + subs.save(self.dest_srt_file) + add_translator_info(self.dest_srt_file, f"# Subtitles translated with Lingarr # ") + except OSError: + logger.error(f'BAZARR is unable to save translated subtitles to {self.dest_srt_file}') + jobs_queue.update_job_progress(job_id=job_id, + progress_message=f'Translation failed: Unable to save translated ' + f'subtitles to {self.dest_srt_file}') + raise OSError + + message = (f"{language_from_alpha2(self.from_lang)} subtitles translated to " + f"{language_from_alpha3(self.to_lang)} using Lingarr.") + result = create_process_result(message, self.video_path, self.orig_to_lang, self.forced, self.hi, + self.dest_srt_file, self.media_type) + + if self.media_type == 'series': + history_log(action=6, + sonarr_series_id=self.sonarr_series_id, + sonarr_episode_id=self.sonarr_episode_id, + result=result) + else: + history_log_movie(action=6, + radarr_id=self.radarr_id, + result=result) + + jobs_queue.update_job_progress(job_id=job_id, progress_value='max') + + return self.dest_srt_file + + except Exception as e: + logger.error(f'BAZARR encountered an error during Lingarr translation: {str(e)}') + jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Lingarr translation failed: {str(e)}') + return False + + @retry(exceptions=(TooManyRequests, RequestError, requests.exceptions.RequestException), tries=3, delay=1, + backoff=2, jitter=(0, 1)) + def _translate_content(self, lines_list, job_id): + try: + source_lang = self.language_code_convert_dict.get(self.from_lang, self.from_lang) + target_lang = self.language_code_convert_dict.get(self.orig_to_lang, self.orig_to_lang) + + lines_payload = [] + for i, line in enumerate(lines_list): + lines_payload.append({ + "position": i, + "line": line if line and line.strip() else 'a' + }) + + title = get_title( + media_type=self.media_type, + radarr_id=self.radarr_id, + sonarr_series_id=self.sonarr_series_id, + sonarr_episode_id=self.sonarr_episode_id + ) + + if self.media_type == 'series': + api_media_type = "Episode" + arr_media_id = self.sonarr_series_id or 0 + else: + api_media_type = "Movie" + arr_media_id = self.radarr_id or 0 + + payload = { + "arrMediaId": arr_media_id, + "title": title, + "sourceLanguage": source_lang, + "targetLanguage": target_lang, + "mediaType": api_media_type, + "lines": lines_payload + } + + logger.debug(f'BAZARR is sending {len(lines_payload)} lines to Lingarr with full media context') + + headers = {"Content-Type": "application/json"} + if settings.translator.lingarr_token: + headers["X-Api-Key"] = settings.translator.lingarr_token + + response = requests.post( + f"{settings.translator.lingarr_url}/api/translate/content", + json=payload, + headers=headers, + timeout=1800 + ) + + if response.status_code == 200: + translated_batch = response.json() + # Validate response + if isinstance(translated_batch, list): + for item in translated_batch: + if not isinstance(item, dict) or 'position' not in item or 'line' not in item: + logger.error(f'Invalid response format from Lingarr API: {item}') + return None + return translated_batch + else: + logger.error(f'Unexpected response format from Lingarr API: {translated_batch}') + return None + elif response.status_code == 401: + raise RequestError("Authentication failed: Invalid or missing API key") + elif response.status_code == 429: + raise TooManyRequests("Rate limit exceeded") + elif response.status_code >= 500: + raise RequestError(f"Server error: {response.status_code}") + else: + logger.debug(f'Lingarr API error: {response.status_code} - {response.text}') + return None + + except requests.exceptions.Timeout: + logger.debug('Lingarr API request timed out') + raise RequestError("Request timed out") + except requests.exceptions.ConnectionError: + logger.debug('Lingarr API connection error') + raise RequestError("Connection error") + except requests.exceptions.RequestException as e: + logger.debug(f'Lingarr API request failed: {str(e)}') + raise + except (TooManyRequests, RequestError) as e: + logger.error(f'Lingarr API error after retries: {str(e)}') + jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Lingarr API error: {str(e)}') + raise + except Exception as e: + logger.error(f'Unexpected error in Lingarr translation: {str(e)}') + jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Translation error: {str(e)}') \ No newline at end of file diff --git a/roles/bazarr/files/teste.yml b/roles/bazarr/files/teste.yml new file mode 100644 index 0000000000000000000000000000000000000000..3246ba4d50aa570f100bc602e8852f1461a2f637 GIT binary patch literal 13014 zcmeI3O>Z1U5QgiF#D5qr$e3j9U4L7OP@Fgs{m7O$dJwanB(K z&N*=O0SO@i&s*)u&g{(EyN<0OdDNPAZ%_5dyj5M*-BtegpG`Zo1KYD>+qN#HjKLWl!?v+X0jf`*iS+p~Q%WyB**_{!@d+o2R$3%%Ng=4W(j z6Z#jPS6thwr|X~%!#C<{^=vpwF_;{YI&Niz-9E@fSRK_ySd1oEO@P{G{CWXiF;CZ) zy^j^>esK$ROikqYcWD>%Yl`&JQ-zvsxS+gc6{|DaA#dw&?7(iqA@N4kP0BI#$MyyJ zA*0{2EoyGUx4QGNZa>c^eBt#ZKw~t)l<7qTC?x$DPOu|-`k2U@N1RU zF_b;Vs59OzFSYz@hNJS(*6o2k<_nK#*RkictTSRFuu#zKk>i9;Cg+(zW50;CH)kgqM%h3Qq`udYBMeyeX4+2G1c`#@1bjvVhu0N zB~+!mw9bUr3(&0C9M3%ZrNz4$o8`B-u!2@NpsvB2#_6h5n;JZ*IsG)WHz`-qE;Z7! zb4U52eNH~@HPhbP_2NS!uflN?E!hKE>E=W5lAf3K(Ank@by4QM&M88@NsIKEn-+FN=*_Ay@aMiwu!Z(Qd@@N*u{_IuBB zd=Y9w46661!z*g zB@lHSX*J;;;lF&n=d3}-&Hz*UrlVgR!fuKA}nW@UZkBj z?2aRU$KUF`Y++bg$?mJgySm%^#8yY_B9=*2|Au+yr$;?J#MjU%UjrIb%6A0)^ zgGKB-I=)AW>?iov^d~L#LP@(fo%O|I^)23nwV-i&*N8E@$ToQBD&Tgd4a)Y|(usnNNqO8cM#wa&9L+lBtsyVV-bv6A*SDQMY5zpl}{ zb|%Lp>G%GKlKow%r3uEfV?h@#T!}6eay5`>yccq^vBLa)kXv^=uL8NHD?)AwUe1Ht z#br+W)TJTF4a!bhTEi|(5|)*>pj)$&Ihy6sZ9-THvf$&a=~*zCIpw|1;Fq<-+5wrc zsxOXj)%SnJk~tc_S*g5Ce204svbIsY7*1Py7xJe#A*Lt2^BKGmF1-uso8t66bm?7? z7pnLUKS0Kzj3!_WTRZ~hTOYcJIUAUN> zNqS1}!iSF;O7FE*F1-uslgH@gac^Er??UicmEMKX?s!V?LgF16^t+Vah5v{5rt~f( zeuV5DZc>}Q?H#SXayg8j~c}&bn%lanxH~S{+$rzA)lz?8Df5i_(-);WQ3woLo83l zJ*v@R5U2_x^_oSH6b#g^ONb5w*UetLy`GOULwn|=u?w+?% zEYfY-H9Qttk%WspitoK`UlTL&h>~JsSMaRf<*XZ#-z6q$j*|T7_jwferkJ;{n2#bO X^#3G^w2?Qj;*mq*i~d(dXS4qR_qsWK literal 0 HcmV?d00001 diff --git a/roles/lingarr/files/lingarr-configmap.yaml b/roles/lingarr/files/lingarr-configmap.yaml new file mode 100644 index 0000000..e999cea --- /dev/null +++ b/roles/lingarr/files/lingarr-configmap.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: lingarr-configmap + namespace: stack-arr +data: + Lingarr.Server.runtimeconfig.json: | + { + "runtimeOptions": { + "tfm": "net9.0", + "frameworks": [ + { "name": "Microsoft.NETCore.App", "version": "9.0.0" }, + { "name": "Microsoft.AspNetCore.App", "version": "9.0.0" } + ], + "configProperties": { + "System.GC.Server": true, + "System.Globalization.Invariant": false, + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Reflection.NullabilityInfoContext.IsSupported": true, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, + "Npgsql.EnableLegacyTimestampBehavior": true + } + } + } \ No newline at end of file diff --git a/roles/lingarr/files/lingarr-deployment.yaml b/roles/lingarr/files/lingarr-deployment.yaml index 86c328c..d34f1ed 100644 --- a/roles/lingarr/files/lingarr-deployment.yaml +++ b/roles/lingarr/files/lingarr-deployment.yaml @@ -15,10 +15,12 @@ spec: spec: containers: - name: lingarr - image: lingarr/lingarr:latest + image: lingarr/lingarr:main ports: - containerPort: 9876 env: + - name: TZ + value: "UTC" - name: ASPNETCORE_URLS value: "http://+:9876" - name: WHISPER_BASE_URL @@ -27,10 +29,48 @@ spec: value: "auto" - name: TARGET_LANGUAGE value: "pt" + - name: DB_CONNECTION + value: postgresql + - name: DB_HOST + value: 'stolon-proxy-service.postgresql.svc.cluster.local' + - name: DB_PORT + value: '5432' + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: lingarr-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: lingarr-secret + key: password + - name: DB_DATABASE + valueFrom: + secretKeyRef: + name: lingarr-secret + key: maindb volumeMounts: - name: config mountPath: /app/config + - name: runtimeconfig + mountPath: /app/Lingarr.Server.runtimeconfig.json + subPath: Lingarr.Server.runtimeconfig.json + readOnly: true + - name: tv + mountPath: /tv + - name: anime + mountPath: /anime volumes: - name: config persistentVolumeClaim: claimName: lingarr-config-pvc + - name: runtimeconfig + configMap: + name: lingarr-configmap + - name: tv + persistentVolumeClaim: + claimName: sonarr-tv-pvc + - name: anime + persistentVolumeClaim: + claimName: sonarr-anime-pvc diff --git a/roles/lingarr/files/lingarr-secret.yaml b/roles/lingarr/files/lingarr-secret.yaml new file mode 100644 index 0000000..7d90cf8 --- /dev/null +++ b/roles/lingarr/files/lingarr-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: lingarr-secret + namespace: stack-arr +type: Opaque +data: + username: dXNlcm5hbWU= + password: cGFzc3dvcmQ= + maindb: bWFpbmRiLXByb3dsYXJy \ No newline at end of file diff --git a/roles/lingarr/tasks/main.yml b/roles/lingarr/tasks/main.yml index c43e939..b6029bf 100644 --- a/roles/lingarr/tasks/main.yml +++ b/roles/lingarr/tasks/main.yml @@ -18,18 +18,18 @@ mode: '0644' -#- name: Obter várias notas do Bitwarden -# shell: | -# echo "unlock" -# BW_SESSION=$(bw unlock {{ bw_password }} --raw) -# echo "get item" -# bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }} -# loop: -# - { id: "iac.ansible.stackarr.radarr.secret", dest: "/tmp/stack-arr/radarr/kubernetes-files/files/radarr-secret.yaml" } -# args: -# executable: /bin/bash -# environment: -# BW_PASSWORD: "{{ BW_PASSWORD }}" +- name: Obter várias notas do Bitwarden + shell: | + echo "unlock" + BW_SESSION=$(bw unlock {{ bw_password }} --raw) + echo "get item" + bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }} + loop: + - { id: "iac.ansible.stackarr.lingarr.secret", dest: "/tmp/stack-arr/lingarr/kubernetes-files/files/lingarr-secret.yaml" } + args: + executable: /bin/bash + environment: + BW_PASSWORD: "{{ BW_PASSWORD }}" - name: Listar conteúdo do diretório remoto diff --git a/roles/soularr/tasks/main.yml b/roles/soularr/tasks/main.yml index 48aca71..cff10e1 100644 --- a/roles/soularr/tasks/main.yml +++ b/roles/soularr/tasks/main.yml @@ -1,18 +1,18 @@ -- name: Remover o diretório /tmp/stack-arr/lidarr/kubernetes-files +- name: Remover o diretório /tmp/stack-arr/soularr/kubernetes-files ansible.builtin.file: - path: /tmp/stack-arr/lidarr/kubernetes-files + path: /tmp/stack-arr/soularr/kubernetes-files state: absent - name: Criar diretório temporário no remoto file: - path: /tmp/stack-arr/lidarr/kubernetes-files + path: /tmp/stack-arr/soularr/kubernetes-files state: directory mode: '0755' - name: Copy file with owner and permissions ansible.builtin.copy: src: ../files - dest: /tmp/stack-arr/lidarr/kubernetes-files + dest: /tmp/stack-arr/soularr/kubernetes-files owner: fenix group: root mode: '0644' @@ -25,7 +25,7 @@ echo "get item" bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }} loop: - - { id: "iac.ansible.stackarr.lidarr.secret", dest: "/tmp/stack-arr/lidarr/kubernetes-files/files/lidarr-secret.yaml" } + - { id: "iac.ansible.stackarr.soularr.secret", dest: "/tmp/stack-arr/soularr/kubernetes-files/files/soularr-secret.yaml" } args: executable: /bin/bash environment: @@ -33,7 +33,7 @@ - name: Listar conteúdo do diretório remoto - shell: ls -l /tmp/stack-arr/lidarr/kubernetes-files/files + shell: ls -l /tmp/stack-arr/soularr/kubernetes-files/files register: resultado_ls @@ -46,6 +46,6 @@ become: yes become_user: fenix shell: | - kubectl apply -f /tmp/stack-arr/lidarr/kubernetes-files/files/ + kubectl apply -f /tmp/stack-arr/soularr/kubernetes-files/files/ environment: KUBECONFIG: /home/fenix/.kube/config \ No newline at end of file diff --git a/roles/soulseek/files/soulseek-deployment.yaml b/roles/soulseek/files/soulseek-deployment.yaml index de4eb08..3cf204d 100644 --- a/roles/soulseek/files/soulseek-deployment.yaml +++ b/roles/soulseek/files/soulseek-deployment.yaml @@ -60,6 +60,8 @@ spec: env: - name: SLSKD_FLAGS_NO_SQLITE_POOLING value: "true" + - name: SLSKD_FLAGS_VOLATILE_AGENT_TOKEN + value: "true" - name: TZ value: 'Etc/UTC' - name: UID @@ -70,7 +72,7 @@ spec: - name: app mountPath: /app - name: media - mountPath: /data + mountPath: /downloads volumes: - name: app persistentVolumeClaim: