lingarr correction

This commit is contained in:
Tomás Limpinho
2026-04-30 13:03:00 +01:00
parent bdd082160f
commit e200933337
9 changed files with 314 additions and 22 deletions

View File

@ -17,7 +17,7 @@ spec:
- name: regcred - name: regcred
containers: containers:
- name: bazarr - name: bazarr
image: lscr.io/linuxserver/bazarr:1.5.3 image: lscr.io/linuxserver/bazarr:1.5.6
securityContext: securityContext:
capabilities: capabilities:
add: add:
@ -26,6 +26,8 @@ spec:
- containerPort: 6767 - containerPort: 6767
name: webui name: webui
env: env:
- name: BAZARR_DEBUG
value: "true"
- name: PUID - name: PUID
value: "1013" value: "1013"
- name: PGID - name: PGID
@ -58,6 +60,10 @@ spec:
mountPath: /config mountPath: /config
- name: media - name: media
mountPath: /media mountPath: /media
- name: lingarrpy
mountPath: /app/bazarr/bin/bazarr/subtitles/tools/translate/services/lingarr_translator.py
subPath: lingarr_translator.py
readOnly: true
volumes: volumes:
- name: config - name: config
persistentVolumeClaim: persistentVolumeClaim:
@ -65,6 +71,9 @@ spec:
- name: media - name: media
persistentVolumeClaim: persistentVolumeClaim:
claimName: bazarr-media-pvc claimName: bazarr-media-pvc
- name: lingarrpy
configMap:
name: bazarr-lingarrpy-configmap

View File

@ -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)}')

Binary file not shown.

View File

@ -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
}
}
}

View File

@ -15,10 +15,12 @@ spec:
spec: spec:
containers: containers:
- name: lingarr - name: lingarr
image: lingarr/lingarr:latest image: lingarr/lingarr:main
ports: ports:
- containerPort: 9876 - containerPort: 9876
env: env:
- name: TZ
value: "UTC"
- name: ASPNETCORE_URLS - name: ASPNETCORE_URLS
value: "http://+:9876" value: "http://+:9876"
- name: WHISPER_BASE_URL - name: WHISPER_BASE_URL
@ -27,10 +29,48 @@ spec:
value: "auto" value: "auto"
- name: TARGET_LANGUAGE - name: TARGET_LANGUAGE
value: "pt" 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: volumeMounts:
- name: config - name: config
mountPath: /app/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: volumes:
- name: config - name: config
persistentVolumeClaim: persistentVolumeClaim:
claimName: lingarr-config-pvc claimName: lingarr-config-pvc
- name: runtimeconfig
configMap:
name: lingarr-configmap
- name: tv
persistentVolumeClaim:
claimName: sonarr-tv-pvc
- name: anime
persistentVolumeClaim:
claimName: sonarr-anime-pvc

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: lingarr-secret
namespace: stack-arr
type: Opaque
data:
username: dXNlcm5hbWU=
password: cGFzc3dvcmQ=
maindb: bWFpbmRiLXByb3dsYXJy

View File

@ -18,18 +18,18 @@
mode: '0644' mode: '0644'
#- name: Obter várias notas do Bitwarden - name: Obter várias notas do Bitwarden
# shell: | shell: |
# echo "unlock" echo "unlock"
# BW_SESSION=$(bw unlock {{ bw_password }} --raw) BW_SESSION=$(bw unlock {{ bw_password }} --raw)
# echo "get item" echo "get item"
# bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }} bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }}
# loop: loop:
# - { id: "iac.ansible.stackarr.radarr.secret", dest: "/tmp/stack-arr/radarr/kubernetes-files/files/radarr-secret.yaml" } - { id: "iac.ansible.stackarr.lingarr.secret", dest: "/tmp/stack-arr/lingarr/kubernetes-files/files/lingarr-secret.yaml" }
# args: args:
# executable: /bin/bash executable: /bin/bash
# environment: environment:
# BW_PASSWORD: "{{ BW_PASSWORD }}" BW_PASSWORD: "{{ BW_PASSWORD }}"
- name: Listar conteúdo do diretório remoto - name: Listar conteúdo do diretório remoto

View File

@ -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: ansible.builtin.file:
path: /tmp/stack-arr/lidarr/kubernetes-files path: /tmp/stack-arr/soularr/kubernetes-files
state: absent state: absent
- name: Criar diretório temporário no remoto - name: Criar diretório temporário no remoto
file: file:
path: /tmp/stack-arr/lidarr/kubernetes-files path: /tmp/stack-arr/soularr/kubernetes-files
state: directory state: directory
mode: '0755' mode: '0755'
- name: Copy file with owner and permissions - name: Copy file with owner and permissions
ansible.builtin.copy: ansible.builtin.copy:
src: ../files src: ../files
dest: /tmp/stack-arr/lidarr/kubernetes-files dest: /tmp/stack-arr/soularr/kubernetes-files
owner: fenix owner: fenix
group: root group: root
mode: '0644' mode: '0644'
@ -25,7 +25,7 @@
echo "get item" echo "get item"
bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }} bw get item "{{ item.id }}" --session $BW_SESSION | jq -r '.notes' > {{ item.dest }}
loop: 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: args:
executable: /bin/bash executable: /bin/bash
environment: environment:
@ -33,7 +33,7 @@
- name: Listar conteúdo do diretório remoto - 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 register: resultado_ls
@ -46,6 +46,6 @@
become: yes become: yes
become_user: fenix become_user: fenix
shell: | shell: |
kubectl apply -f /tmp/stack-arr/lidarr/kubernetes-files/files/ kubectl apply -f /tmp/stack-arr/soularr/kubernetes-files/files/
environment: environment:
KUBECONFIG: /home/fenix/.kube/config KUBECONFIG: /home/fenix/.kube/config

View File

@ -60,6 +60,8 @@ spec:
env: env:
- name: SLSKD_FLAGS_NO_SQLITE_POOLING - name: SLSKD_FLAGS_NO_SQLITE_POOLING
value: "true" value: "true"
- name: SLSKD_FLAGS_VOLATILE_AGENT_TOKEN
value: "true"
- name: TZ - name: TZ
value: 'Etc/UTC' value: 'Etc/UTC'
- name: UID - name: UID
@ -70,7 +72,7 @@ spec:
- name: app - name: app
mountPath: /app mountPath: /app
- name: media - name: media
mountPath: /data mountPath: /downloads
volumes: volumes:
- name: app - name: app
persistentVolumeClaim: persistentVolumeClaim: