ptman (6627B)
1 #!/usr/bin/env python3 2 import os 3 import sys 4 import json 5 import subprocess 6 import urllib.request 7 import urllib.error 8 import time 9 import re 10 from concurrent.futures import ThreadPoolExecutor 11 12 API_URL = "https://translate.disroot.org/translate" 13 TARGET_DIR = os.path.expanduser("~/.local/share/man/pt") 14 CHUNK_LIMIT = 3000 15 TRANSLATE_WORKERS = 3 # nº de chunks traduzidos em paralelo 16 REQUEST_DELAY = 1.0 # respiro entre disparos, por worker 17 18 19 def clean_backspaces(text): 20 """Remove sequências de overstrike (char + backspace) usadas para negrito/sublinhado.""" 21 while '\x08' in text: 22 text = re.sub(r'.\x08', '', text) 23 return text 24 25 26 def split_long_line(line, limit): 27 """Quebra uma única linha maior que o limite em pedaços menores.""" 28 return [line[i:i + limit] for i in range(0, len(line), limit)] or [line] 29 30 31 def translate_chunk(text): 32 """Envia um bloco de texto para a API de tradução, com retentativas.""" 33 payload = {"q": text, "source": "auto", "target": "pt", "api_key": ""} 34 req = urllib.request.Request( 35 API_URL, data=json.dumps(payload).encode('utf-8'), 36 headers={'Content-Type': 'application/json'}, method='POST' 37 ) 38 for attempt in range(5): 39 try: 40 with urllib.request.urlopen(req, timeout=30) as response: 41 res_data = json.loads(response.read().decode('utf-8')) 42 return res_data.get("translatedText", "") 43 except urllib.error.HTTPError as e: 44 if e.code == 429: 45 time.sleep(3 * (attempt + 1)) 46 continue 47 return None # erro definitivo do servidor 48 except Exception: 49 time.sleep(2) 50 continue 51 return None 52 53 54 def build_chunks(text): 55 """Divide o texto em blocos respeitando CHUNK_LIMIT.""" 56 lines = text.splitlines() 57 chunks, current_chunk, current_length = [], [], 0 58 59 for line in lines: 60 if len(line) > CHUNK_LIMIT: 61 if current_chunk: 62 chunks.append("\n".join(current_chunk)) 63 current_chunk, current_length = [], 0 64 chunks.extend(split_long_line(line, CHUNK_LIMIT)) 65 continue 66 67 if current_length + len(line) + 1 > CHUNK_LIMIT: 68 chunks.append("\n".join(current_chunk)) 69 current_chunk, current_length = [line], len(line) 70 else: 71 current_chunk.append(line) 72 current_length += len(line) + 1 73 74 if current_chunk: 75 chunks.append("\n".join(current_chunk)) 76 77 return chunks 78 79 80 def translate_text(text, status_prefix=""): 81 """Traduz um texto inteiro, fatiando em chunks e mostrando progresso em paralelo.""" 82 chunks = build_chunks(text) 83 total_chunks = len([c for c in chunks if c.strip()]) 84 if total_chunks == 0: 85 return text 86 87 results = [None] * len(chunks) 88 done_count = [0] 89 90 def worker(idx_chunk): 91 idx, chunk = idx_chunk 92 if not chunk.strip(): 93 results[idx] = chunk 94 return 95 translated = translate_chunk(chunk) 96 results[idx] = translated if translated is not None else chunk 97 done_count[0] += 1 98 if status_prefix: 99 pct = int((done_count[0] / total_chunks) * 100) 100 print( 101 f"\r{status_prefix} -> Traduzindo... {done_count[0]}/{total_chunks} partes ({pct}%)", 102 end="", flush=True 103 ) 104 time.sleep(REQUEST_DELAY) 105 106 with ThreadPoolExecutor(max_workers=TRANSLATE_WORKERS) as pool: 107 list(pool.map(worker, enumerate(chunks))) 108 109 if status_prefix: 110 print(f"\r{status_prefix} -> Concluído!{' ' * 25}", flush=True) 111 112 return "\n".join(results) 113 114 115 def resolve_man_path(args): 116 """Resolve o caminho físico do manual via `man -w`.""" 117 try: 118 output = subprocess.check_output( 119 ["man", "-w"] + args, stderr=subprocess.DEVNULL 120 ).decode('utf-8').strip() 121 except subprocess.CalledProcessError: 122 return None 123 124 candidates = [p for p in output.splitlines() if p.strip()] 125 return candidates[0] if candidates else None 126 127 128 def process_manual(args): 129 """Busca, traduz (se necessário) e exibe o manual especificado.""" 130 src_path = resolve_man_path(args) 131 if not src_path: 132 print(f"Erro: Manual não encontrado para: {' '.join(args)}") 133 return False 134 135 parts = src_path.split('/') 136 if len(parts) < 2: 137 print("Erro: Caminho do manual inválido.") 138 return False 139 140 section_dir, filename = parts[-2], parts[-1] 141 clean_filename = filename.replace('.gz', '') 142 dest_path = os.path.join(TARGET_DIR, section_dir, f"{clean_filename}.txt") 143 144 # Retorna a versão em cache, se existir 145 if os.path.exists(dest_path): 146 subprocess.run(["vim", dest_path]) 147 return True 148 149 print(f"-> Origem localizada: {src_path}") 150 package_origin = "Desconhecido" 151 152 try: 153 apk_out = subprocess.check_output( 154 ["apk", "info", "-W", src_path], stderr=subprocess.DEVNULL 155 ).decode('utf-8').strip() 156 if "is owned by" in apk_out: 157 package_origin = apk_out.split("is owned by")[-1].strip() 158 except Exception: 159 pass 160 161 print(f"-> Pacote: [{package_origin}]") 162 print("-> Traduzindo sob demanda...") 163 164 try: 165 raw_text = subprocess.check_output( 166 ["mandoc", "-Tutf8", src_path], stderr=subprocess.DEVNULL 167 ).decode('utf-8', errors='ignore') 168 raw_text = clean_backspaces(raw_text) 169 170 if not raw_text.strip(): 171 return False 172 173 translated = translate_text(raw_text, f"-> {clean_filename}") 174 175 os.makedirs(os.path.dirname(dest_path), exist_ok=True) 176 with open(dest_path, "w", encoding="utf-8") as f: 177 f.write(translated) 178 179 subprocess.run(["vim", dest_path]) 180 return True 181 182 except Exception as e: 183 print(f"\nFalha na tradução: {e}") 184 print("Abrindo fallback em inglês...\n") 185 time.sleep(1.5) 186 subprocess.run(["man"] + args) 187 return False 188 189 190 def main(): 191 args = sys.argv[1:] 192 193 if not args: 194 print("Uso:") 195 print(" ptman <cmd> (Ex: ptman git)") 196 print(" ptman <sec> <cmd> (Ex: ptman 5 crontab)") 197 print(" ptman clean (Apaga o cache atual)") 198 sys.exit(1) 199 200 cmd = args[0].lower() 201 202 if cmd == "clean": 203 if os.path.exists(TARGET_DIR): 204 subprocess.run(["rm", "-rf", TARGET_DIR]) 205 print("Cache limpo.") 206 else: 207 print("Cache já estava vazio.") 208 sys.exit(0) 209 210 process_manual(args) 211 212 213 if __name__ == "__main__": 214 main()