From b4b2350a1ee661ebb0b53ebc4cf4f5bd350dfe52 Mon Sep 17 00:00:00 2001 From: Tarkiin <61888270+Tarkiin@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:20:08 +0200 Subject: [PATCH 01/10] Add GUI hosts app - main.py --- gui-hosts-app/main.py | 414 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 gui-hosts-app/main.py diff --git a/gui-hosts-app/main.py b/gui-hosts-app/main.py new file mode 100644 index 00000000000..6ef1cd7ac8c --- /dev/null +++ b/gui-hosts-app/main.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Hosts Generator +Generador de archivos hosts personalizados con descarga de diferentes fuentes. +""" + +import os +import sys +import json +import requests +import webview +import threading +import time +import concurrent.futures +from pathlib import Path +from datetime import datetime +from urllib.parse import urlparse + +class HostsGenerator: + def __init__(self): + self.base_dir = Path(__file__).parent + self.data_dir = self.base_dir / "data" + self.output_dir = self.data_dir / "output" + self.sources_dir = self.data_dir / "hosts_sources" + self.lang_dir = self.base_dir / "lang" + + # URLs de fuentes de hosts (directas desde GitHub) + self.sources = { + "base": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + "description": "Base hosts (adware + malware)" + }, + "fakenews": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts", + "description": "Fake news sites" + }, + "gambling": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling/hosts", + "description": "Gambling sites" + }, + "porn": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts", + "description": "Adult content sites" + }, + "social": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts", + "description": "Social media sites" + } + } + + # Estado de descarga + self.download_status = { + "downloading": False, + "progress": 0, + "current_source": "", + "message": "" + } + + # Crear directorios necesarios + self.ensure_directories() + + def ensure_directories(self): + """Crear directorios necesarios""" + self.data_dir.mkdir(exist_ok=True) + self.output_dir.mkdir(exist_ok=True) + self.sources_dir.mkdir(exist_ok=True) + self.lang_dir.mkdir(exist_ok=True) + + def get_download_status(self): + """Obtener estado de descarga""" + return self.download_status + + def check_sources_exist(self): + """Verificar qué fuentes existen localmente""" + existing = [] + missing = [] + + for name in self.sources.keys(): + file_path = self.sources_dir / f"{name}.txt" + if file_path.exists(): + existing.append(name) + else: + missing.append(name) + + return { + "existing": len(existing), + "total": len(self.sources), + "missing": missing, + "all_exist": len(missing) == 0 + } + + def download_sources_async(self): + """Descargar fuentes en hilo separado""" + thread = threading.Thread(target=self.download_sources) + thread.daemon = True + thread.start() + return {"success": True, "message": "Descarga iniciada"} + + def download_sources(self): + """Descargar todas las fuentes faltantes""" + try: + self.download_status["downloading"] = True + self.download_status["progress"] = 0 + self.download_status["message"] = "Iniciando descarga..." + + # Verificar qué fuentes faltan + status = self.check_sources_exist() + if status["all_exist"]: + self.download_status["message"] = "Todas las fuentes ya están disponibles" + self.download_status["progress"] = 100 + return + + # Descargar solo las fuentes faltantes + sources_to_download = [(name, info) for name, info in self.sources.items() + if name in status["missing"]] + self._download_sources_list(sources_to_download) + + except Exception as e: + self.download_status["message"] = f"Error general: {str(e)}" + + finally: + self.download_status["downloading"] = False + + def get_available_extensions(self): + """Obtener extensiones disponibles (incluyendo base)""" + extensions = [] + for name, info in self.sources.items(): + file_path = self.sources_dir / f"{name}.txt" + extensions.append({ + "name": name, + "description": info["description"], + "available": file_path.exists(), + "size": file_path.stat().st_size if file_path.exists() else 0, + "is_base": name == "base" + }) + + return extensions + + def get_available_languages(self): + """Obtener idiomas disponibles""" + languages = [] + if self.lang_dir.exists(): + for lang_file in self.lang_dir.glob("*.json"): + lang_code = lang_file.stem + languages.append({ + "code": lang_code, + "name": "Español" if lang_code == "es" else "English" if lang_code == "en" else lang_code.upper() + }) + return languages + + def get_language_strings(self, lang_code="es"): + """Obtener strings de idioma""" + lang_file = self.lang_dir / f"{lang_code}.json" + if lang_file.exists(): + try: + with open(lang_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error cargando idioma {lang_code}: {e}") + + # Fallback a español si no se encuentra el idioma + fallback_file = self.lang_dir / "es.json" + if fallback_file.exists(): + try: + with open(fallback_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + pass + + # Fallback hardcoded si no hay archivos de idioma + return { + "app_title": "🛡️ Generador de Hosts", + "app_subtitle": "Generador de archivos hosts personalizados", + "error": "Error" + } + + def update_sources_async(self): + """Actualizar fuentes existentes en hilo separado""" + thread = threading.Thread(target=self.update_sources) + thread.daemon = True + thread.start() + return {"success": True, "message": "Actualización iniciada"} + + def update_sources(self): + """Actualizar todas las fuentes existentes""" + try: + self.download_status["downloading"] = True + self.download_status["progress"] = 0 + self.download_status["message"] = "Iniciando actualización..." + + # Descargar todas las fuentes (actualizar) + sources_to_download = list(self.sources.items()) + self._download_sources_list(sources_to_download) + + except Exception as e: + self.download_status["message"] = f"Error general: {str(e)}" + + finally: + self.download_status["downloading"] = False + + def _download_sources_list(self, sources_to_download): + """Método auxiliar para descargar una lista de fuentes""" + total_sources = len(sources_to_download) + completed = 0 + + def download_single_source(source_data): + nonlocal completed + name, source_info = source_data + + try: + self.download_status["current_source"] = f"Descargando {name}..." + print(f"Descargando {name} desde {source_info['url']}") + + # Configuración optimizada para requests + session = requests.Session() + session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + + response = session.get( + source_info['url'], + timeout=45, + stream=True, # Para archivos grandes + allow_redirects=True + ) + response.raise_for_status() + + file_path = self.sources_dir / f"{name}.txt" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(response.text) + + completed += 1 + progress = int((completed / total_sources) * 100) + self.download_status["progress"] = progress + + print(f"✓ {name} descargado exitosamente ({completed}/{total_sources})") + return True + + except Exception as e: + print(f"✗ Error descargando {name}: {e}") + self.download_status["message"] = f"Error en {name}: {str(e)}" + return False + + # Descargas concurrentes (máximo 5 simultáneas) + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(download_single_source, source) for source in sources_to_download] + + # Esperar a que todas terminen + concurrent.futures.wait(futures) + + self.download_status["progress"] = 100 + self.download_status["current_source"] = "Descarga completada" + self.download_status["message"] = f"Descargadas {completed}/{total_sources} fuentes exitosamente" + + def generate_hosts_file(self, extensions): + """Generar archivo hosts personalizado""" + try: + # Verificar que la base existe + base_file = self.sources_dir / "base.txt" + if not base_file.exists(): + return { + "success": False, + "message": "Archivo base no encontrado. Descarga las fuentes primero." + } + + # Generar nombre único + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + if extensions: + ext_suffix = "_" + "_".join(sorted(extensions)) + else: + ext_suffix = "_base" + + output_filename = f"hosts_{timestamp}{ext_suffix}" + output_path = self.output_dir / output_filename + + # Leer archivo base + print(f"Leyendo archivo base: {base_file}") + with open(base_file, 'r', encoding='utf-8') as f: + base_content = f.read() + + # Combinar con extensiones + combined_content = base_content + extension_stats = [] + + for ext in extensions: + if ext == "base": # Skip base ya que siempre se incluye + continue + + ext_file = self.sources_dir / f"{ext}.txt" + if ext_file.exists(): + print(f"Agregando extensión: {ext}") + with open(ext_file, 'r', encoding='utf-8') as f: + ext_content = f.read() + + # Agregar separador y contenido + combined_content += f"\n\n# === {ext.upper()} EXTENSION ===\n" + combined_content += ext_content + + # Estadísticas + ext_lines = len(ext_content.splitlines()) + extension_stats.append(f"{ext}: {ext_lines:,} líneas") + else: + print(f"Advertencia: Extensión {ext} no encontrada") + + # Escribir archivo final + with open(output_path, 'w', encoding='utf-8') as f: + f.write(combined_content) + + # Estadísticas finales + file_size = output_path.stat().st_size + total_lines = len(combined_content.splitlines()) + + stats_message = f"Archivo generado exitosamente\n" + stats_message += f"Nombre: {output_filename}\n" + stats_message += f"Tamaño: {file_size:,} bytes\n" + stats_message += f"Líneas totales: {total_lines:,}\n" + if extension_stats: + stats_message += f"Extensiones: {', '.join(extension_stats)}" + else: + stats_message += "Extensiones: Solo base (adware + malware)" + + return { + "success": True, + "message": stats_message, + "filename": output_filename, + "path": str(output_path), + "size": file_size, + "lines": total_lines + } + + except Exception as e: + return { + "success": False, + "message": f"Error generando archivo: {str(e)}" + } + + def open_output_folder(self): + """Abrir carpeta de salida""" + try: + if sys.platform == "win32": + os.startfile(self.output_dir) + elif sys.platform == "darwin": + os.system(f"open '{self.output_dir}'") + else: + os.system(f"xdg-open '{self.output_dir}'") + return {"success": True, "message": "Carpeta abierta"} + except Exception as e: + return {"success": False, "message": f"Error: {str(e)}"} + + def get_output_files(self): + """Obtener lista de archivos generados""" + files = [] + if self.output_dir.exists(): + for file_path in self.output_dir.glob("hosts_*"): + if file_path.is_file(): + stat = file_path.stat() + files.append({ + "name": file_path.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S") + }) + + return sorted(files, key=lambda x: x["modified"], reverse=True) + +def main(): + """Función principal""" + print("🛡️ Iniciando Hosts Generator...") + + # Crear instancia de la aplicación + app = HostsGenerator() + + # Verificar archivo HTML + html_path = app.base_dir / "ui.html" + if not html_path.exists(): + print(f"Error: No se encontró ui.html en {html_path}") + input("Presiona Enter para salir...") + return + + print(f"📁 Directorio base: {app.base_dir}") + print(f"📁 Directorio de datos: {app.data_dir}") + print(f"📁 Directorio de salida: {app.output_dir}") + + # Verificar estado de fuentes + sources_status = app.check_sources_exist() + if sources_status["all_exist"]: + print(f"✅ Todas las fuentes están disponibles ({sources_status['existing']}/{sources_status['total']})") + else: + print(f"⚠️ Faltan {len(sources_status['missing'])} fuentes de {sources_status['total']}") + print(f" Faltantes: {', '.join(sources_status['missing'])}") + + # Crear ventana de la aplicación + print("🚀 Iniciando interfaz web...") + + webview.create_window( + title="🛡️ Hosts Generator", + url=str(html_path), + js_api=app, + width=1024, # Cambia este valor para el ancho (actualmente 1024) + height=900, # Cambia este valor para el alto (actualmente 750) + min_size=(800, 600), # Tamaño mínimo (actualmente 700x500) + resizable=True + ) + + webview.start(debug=False) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n👋 Aplicación cerrada por el usuario") + except Exception as e: + print(f"❌ Error fatal: {e}") + input("Presiona Enter para salir...") \ No newline at end of file From f2ec8f4385eaa669f4c4757bd0213069a40caa8c Mon Sep 17 00:00:00 2001 From: Tarkiin <61888270+Tarkiin@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:20:22 +0200 Subject: [PATCH 02/10] Add GUI hosts app - requirements.txt --- gui-hosts-app/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 gui-hosts-app/requirements.txt diff --git a/gui-hosts-app/requirements.txt b/gui-hosts-app/requirements.txt new file mode 100644 index 00000000000..5b0b9fca651 --- /dev/null +++ b/gui-hosts-app/requirements.txt @@ -0,0 +1,3 @@ +# Dependencias mínimas para Hosts Generator +pywebview>=4.0.0 +requests>=2.25.0 \ No newline at end of file From 68682570f3bbb412bb2debf67dfc6e52288fbdb6 Mon Sep 17 00:00:00 2001 From: Tarkiin <61888270+Tarkiin@users.noreply.github.com> Date: Thu, 31 Jul 2025 03:20:55 +0200 Subject: [PATCH 03/10] Add GUI hosts app - ui.html --- gui-hosts-app/ui.html | 113 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 gui-hosts-app/ui.html diff --git a/gui-hosts-app/ui.html b/gui-hosts-app/ui.html new file mode 100644 index 00000000000..ccfa077741c --- /dev/null +++ b/gui-hosts-app/ui.html @@ -0,0 +1,113 @@ + + +
+ + +Generador de archivos hosts personalizados
+sJy1rwpRO-S6Uq?8Dq#;(2^HtET{>0RAD2s&OqKGtb|`PVw$wubw9K0psx{ zz*n#LzW5v=j1mJW*nU4^uXiA|R30IB0qK6YQ@!2M*6*BJ=EIs8`HJPH=d5|ob@JO- z^+_f4tFVqU!n6K5yJ9U+reKu@NtF{{|7yXtp4nT}fH+_F4ynBUod5F5y)}eP*M-!s z^;{qo5l_1+J`fElSSKo2oMlnb!KeCS%b!OpQ54x&To{o|lr~A9rUXEY8}+f9^`Tt9 zfn5sF!{Vd%@cwO^0x2q{uzIl(=c6uX?PK~S`#+5%i$QbF=@KXgyQ$hoGt}H2^q08D zeItwbzU55A39A$CtE+9L;c^>sXJs=zIPOaUmrIEO8o$aa;dWH%=q>gq(~39aDj>3s z$7g}?!?7EKb|cRuv;OtdEdg)tErB!C!3=I~cn&U7r?Q|}j 0 zNZ89F#8E>Q0uAu&--WPh)HU~vZ~pPGAP!YoV! 6fx>YS3mp% zI2l4zFog;6Q*S)I4}p5(#=MOT6-K;#>wCN1kkNTDatBlu2f?$_Ug}82i;n$s89Bb& z*vyx`2m9_4Ha|Hl;q4in_*mQLK^br7eGYY0_32X0?D=At$!ioReCLY&j`iVvk6GFmi1gTtP5YcFvF|;zl4$u8;hjo;>m+hY$)w}?AK6iD z!-PR_-M-VQHjxJH_W@Sun@kd;3o3xByTOkQY|SPKjD9JWGl^IP83us;OKY#?nqPfH zk>;3SY{X(tY~?@|fUHR?HHHifsw(_5$P~liprgPM&TQ9nZJf~=T#*u*m3O7!zw1-| zw9;c0ZJ+HyVb%^VR)1~Iy|#Pxa<&YBUSnKQ${JA^<827Bt7M9El;nu-Mvj>uoRqGw zU{#N30g*9C?thT>o?%Ty;l3}3fJzgQBGn2ay?0O%QL2d2i-7b_=q(^BQUsOWf`SyK z_ny!rHBv)QsG$c42_-=8;68irbI!fz!+Gw9Jb}z4GxN+^Yu@!=@B9DB=qML_F2KUO z?6cm1?E5ONVK^6&?TOlT?cBueaEgOjN(J{8a^Bt#gy^Hx-7JaSGzITLzeUolR@ntg zQ$LD0IpLlVypK$)-SpNcV0Q}Y?FO#%q=mzfW4FwMHFxWev%jowW*?BQ!b6uh&Gv{5 zlWNupLg_wyZuL@w;$&ta>->j%B{(J(CO+LZy9}?%w=Yv;?lr6Aap!ypw76fZk4Y@j zQ5yRUTgx~5cBol}dudGwru<%t?XKoQMe&WXrj|gVhbS^8xN_tT)Bx
#iJdP;oL&S!b9;ZDpVMA3s&Fs4AIfEU)QR+z`U?yS1-jyKywCC2z-iRt zLRofrA2fhPScxRa>DSq2bvl`S#%kuPJeDoDbulw&MN%u^h{}Ip+!Q@+c`E3O3WzL^ z`9bB2vZc=STany^Y8I;yM@a90SmI)#<53mq_}2NC%mUAh>knrnb_VNOD5VMr=Wndp zK~%yDB(3x9u{<(y>PCxM?m;xdatFBj{gWz>$s>E9i_*7^np?!jpW^h4z{Bzr&UG^A zW*}B^t(&9O_X|*h;>W-pwwUaw7{sX}9NmEK{3cPjDk`G=R_0#sUb*WnNc~8t1<~YW zjUf ^I`ua7OUQbJVFT zQg}AAJ1Rohe`qC**#e^$f?+&l?Ru L*Ef09$aUK?Qb{nYZ2dEH7Zmaqjc=BpAvRX&G{DQigY^W`7%G=aWj4I z%AibFu<2@F)h(8-Oc7Kf!IYBa!5$|&FMkq{)suR497bWEUB~3X(c%=^c^NrB=i{40 zBTuLvGGG#0d_UsRdD$M_&(Xt&Rh;>GPZe;=yf`p0@9nOBt*5GjeP3uy^ n(RX7i06`P>r)PmgITey7^2nm2Uy3~nS(uQR-5+Z zAnPd$m3BVe6RwpvV%sh2-obk>$*@X{sA=ZI<*0(iKk=DycgoVEo;N|>IR I&=H1*?UKpfo7u6BysfjcAGbwfPK+W5f<_|B0@g5>g-Z>CP; z2Ku7zpB_a9ud)4-vhmJ0@HBt!R|8aQ2`wfm2`vK#RbF+s9Nf+;;0>+|jBrmHzZTO6 zz}38l!H=esxL^Nx7(K(gX1A`L@|0^kS# ^$nJRpzAW0r`nu>e|{bImQ6-<{&s^)$*6=) z(j@1pAZpH&YZZ7Tz+j5O;~g PjZbGc$M@D+&s4{A*c38v}vF^A%kg+t2Xq zm<3g=Qi3{c)_{yx84igv>}}5DLB#wwxj?s;=hX=VnG#% tZT2tmz9 zlTKusvHJUqLu*hjA^M0q8(H07(!L>53@C*y*=B5W{-NJOFO72Kfp(L!g2=u6b*3}4 zU^5m4ezz8{$ck||Pu*J5G28o~yHGEX(r7azjmgOF>@l2+G9DyA3W89_N}Tvo-GyRO z6E#q%=8twI9a7X2-XRvzxzJbv%-_xT6%*fI^=u5tv5L>HXPUnDUlx#V*9|3THXj^1 z)~_M`-XODT;oMEzVfxB}v}EX=#e(_sS7Oy*M^r)PhspSXE*>oS63<}@Vj=m4-*4AH zwA1}A>MR*8UROc`HrzcAcSl4|2@0OAbNUr m z>7cye=$3W?nNYdVlMzec5V~z5p&``1vui$N4}zCfppgX_kxp$Gl!7mF4_o+Rz8pNm zQ+G;W@W<{Jp!rlzndkQM&C7uUcwI|0>JnUQT_EGoV5rg#Hb3kqY)HT60qlFEvO8!H zB@t3b>v 0?^ks*ES<3Dv4$BmfE0=G(c z-x1^MYz8}a9|!ei&CyPTa9z8aM(=GW0AxM@L>w_Z+Z*GtU$5{+2Q#X+6j~-ssib|O zA37@^dxcW#t{P$Q1?`lxEY3N@K061@8v6}UNC)4GE6HCNMe25EP7k)JDZo-~W9Rqs~3Nq;#equj?k|!OdV~-}BRNoAmquQhj!`4|cfz*M&K^qS^{fC{)hnQepe8S7L0ZO$~ zm*JlZ__*f{Pi!$uu0q6bqeUToPHT{HoRL(LLiXK#5M>bsqttqIsE)70B_zV^BhMU+ z)=;IKHUarPJ1y&A^NN{2@|xnQV$F}oQETey6uO=UJjg!_z}0ls)vue@^>^Z&-QFXy zbe#3GPg8f=wNCcLArXCpT_V|Ew(HQvy1-xv+qtu+PmY|Ch^)=(w>6sM8mY~Wm8;8& ziTQb!k6i$BU#y)y<*LZ~(O)zcpnv6AqmZp-^4yxTaAO*? R *e`}r6wu?>eg@);Vjqayr7Ml{ zb7S8WL>}fTgT&q|gdRA?Y2u#tQzh%A > z+( *A6@x zTlY8MqE|TtSVr4Kmkzec^>zYP7%3PTVHD(5=iXty+y0UhpXKTzHT_7I$l*@rro^0m z2sKyt-CVP59i%Dks_=9FOv8}&=?2dkyKrJs5SWaWyOPoFbcb6@eu*yioA14#1$xf2 zhAZ~q2#ZH&od*uh)S*j~oM+K;_4}5v56=C#Bnn-5y2vu3I_K_D(IPHEG=zFy6E$%e z?^-aQfQldSYgI2q%xA;c;i1R0n;%dMpG?O)D5>4wt62I_K|ONo eQp zh&LuQEuL$;aJ$LRb3Qx?hRk6SOjDGKyW^%?*i$TgK2Ji=2+BZ;;Z9S~_QnGAPXS*G z)1vgmCFf!l@+KW)I-3V*Wc+XpSCe})yG89d8(C|D21IydZpHEX-DSux T`Gzh$A|sSOXu(f* zFP7(nH^bo{{>=k*D>gWUAh&s=cEoxnXe>>3gB5!T>KLHzl9>ODaPf$GFTk;nt9F__ z#XN-T_4Rn9#kt9Zg~~DooBnIE)e8xMxYxJCey !}Yg+7Dc{-?&M3Qt#Dat{0*Nmkf3c;`}*RMf^tC2@bxrT#y`UB z&lZP%*jAr@S-FYi2M1zG4q+Q^9eswTpTHpl*}IjV`_EfG;F7J5m|}KH@A+8~r6Aok zGN@?M(Ggu@n)XayV&OpkW3^u&0(+m2QO~|p)EN3-{Mo 8$5 2_kj)&r=?A75%=jB4XBgIw^xyWqz<((MNwsFeK5XWP@4GyS|v! zfRum8HA=Gbv!6a5Q@~P5B@WT<$j22y$6FSUA2(N5)Gy8S_&uq9ZU|rL=5J?SJ@(bv zYM_S2S`195PDvJ%{h|5tU3VVi@Y=2^Q|>TVR6U=xne#0cMeS?~R+{NFff_>3^w<1M zOcr3bQD`|&U*?dp$g-*yP5JD?ciZO{Bjff}OA}Y#mYE$03b?qX>sp`D?Dy7i(IhE} z25Ze;C66yO z=8n#xWD?T{OSsw;6GBsmp#PjvJEYLKHbK1WbG5VYImd#VKi0`?1XF?JqCUaV7cDL` zg#{LNx8GBhb=dHfeVgGqfk|3?!7$c(HH5v=DR^ge$14wnmRid(FDn+e`RUjuRxo5= zb6{rAD@^*cCcEbpYIvb830kR$kq)J45MEE({R_QU^u32%sP|IiH+brrH}aX!K6xtg zUgx{2YuakHJbOzi(FCje7{)D=o(o&eYzE9&fqPz>+T5ByHBe52Raau+8ecy%INA`O z%#L;cEvjBoS~Gwn?WPl0Z%^toElYeku9|$jm?+tEn`1y)`e$p!vxg~;BU(W|Y4Ok4 zdEF_tP5Uqmo&UKu4)DMngPtilS|rO^hm0usp(~3*KZ+sx;a@Wp--9Q1$_|_CdD`b~ zw%ZfOSmwX+SjKR;!&LF{!U;K#@0s}EyzfSt=@$qmswYHG5%9skGq26Y4f8t)|5~}i zKm2grCo;C%>Ti^4m9At$@{=GT`Ij%tUuAg %wJlM(hc-S o&_uSqBVyaD%t)J-*oYHFzd6X!zIpw@cvJZ&f%PukH*KJeH?w%YpJHRR$VM$W zH`|m~@Cpa*NqggX(-6Amb*ZK|eOZK;L`ku2BT9`G1M0XiIyu&nmfpR??bb}0#RIT; z?B%kFE7ZR$dXGw!gZY~61_J!iLE@H7Vn+kjW=N^^&%D+bSgf``<97OKT7TjTUWSM( z?Hy=KR{cz~u7!3h_G64*s4qX=7@ofQ?My~F;>d_1VTq6Z1w~L<16*$-XTsxV5ci&ydAdLOhuUW z=IkzvxqI9!&Ag$HBSB0q7E4^FejhlvHIN{BIYtZ|_o_7fxyS($VO-B mxfy*yu?4c!K}zX-@6v>l2z(9zxC5c0fd&MyrNB7qm^6YfZ6bvaN>GD zMOB*%(}pTFqcrohf!LD0A=8y=+)71Uu~368lh3gM2;y@y8^IZ^8}L)3dUQ%m)OLr< zMJsPx_%9dh(5?m~S#dag^X07TO#lY4m-RY(GuF7_(IqurJ45!Dtwv9Z6vTcm<}9&} zZ T$4wj#0HW!w@@i$6MG6!i^Ye G7AHOT&?FEe*a2o@XZ? z3dvlWmhB23pz8sxs#~PMeK>`))_;sQwXrV__>_wXyRm3sN(T;n!@`|;bjhOf>~8ah zEAc|%PAMRQiA @e9K;`Pm&* YgC z3JJG+XZj!&wI-ZIG8^MF^$QjfMY6(abf@mP_$ejl*hMegc##<=Avew+1UhBpvnr!w zU7RyrErC}M<2^ @zg 7Hh z+Hjq9X7_@D2gi=C(j@0Ki(|v|rg*L8z$>85?F_4S8juH4+q0j;6)ZW|>&V$ev`gLT zY?0mIMW=(XU71oc9gs-Sbcb%NJ8RiP)^(0uFFbwl?#)Txwcrc;r{qkvGYO%79N!V1 z;9bxmB7T#5>K;zWyhQk$o%k(rq?j_&QuA1w*AV805Ew+_@sssvt+j~tsnHCe2^tAklJfaT|d@sHSu+S=88$5 z?z69VoZCVr `x9E@TR;bnLG;#4q#!_SC}kH_ciYxBZ6%PMm)_O&$A z$SRWmMn`IW@^Ml)OKtGc%Vz9@sqBo+)&%9x9d*MT!1TdOIs3GqsWp+VVHVI7=Chj~ z?3XJ;z-W2Spuqt$dN%+-n#O$~F)eO7y!|P`W36=W##z_l;%j?!F^8jchkdOI*k?4D zQ3lA+G;$X><~&G~xqzP|y{BO?VD%K~@3iG1Nte!N(>fodZTap4t1Q#cLhXn>QP|FU zJg*HNzi?14GwHPWKrkqfemv7B%{W26RFrvSaE@WR^jEs#t|@0gH)$te^E?HGE1Yk| zDwi(6v_+ID1-#I7sIt4alIt1XPok0V#I_HV3x%p@Pb!~03D$L83@U46O`xA+OyF+j z<~cbQlKrUCcdEESq2hIAt}1VeR6jSUE)YG5aIvSGu2dPxp5$GOnUfD)YW&@O75?YQ zmbNCDLL+&=k|oRVwG?K;ImD^4B#m0`hep%VMN@QnKgdCj7B_Rl_q5*^-!{VSoSKy6 zK!{H|y|d!lJUCv;WKK4H^d)Bb(4eOOknDSmt;u7$(bw9ro)>x<9Uo-B}t;_kk_?PNRp5N(aV+g0XGX=)oqe1pWy3IJLj-#T7DdVYXg7HmQG?Dhu~0aR}^lF?7qjgvcjNuJUB>-DKeG@ zuuodUv-366Ky+Hx7l}+7JQ{?QzBHf5bDT0+uD%p$p$(u+R%^aF-e=tiHc1(s#yske zX73_jg^?C0(DoxH3@K-4SubnqFlF6Gw$dgLy+J!-*TAfBEqqkbL)!$%ITpVIaNm{k zlpWNFH=Yg6*_S^7Rsz2`Pd@Dv_bt3aX|N=%?eYKslW%<0oMG>!6sa$j_)nCfHps^3 z{|2~WHL;sIDvFG*zH1_q*C=)GC%~E#3oaE!hF5=={9Lhn@-|qS>HkeKEXs&_`p@() z{oM4ATI1K^r8jYMfWx^`O&mA*6+wJdX|8a>Y~uYcr+>k!zoR7R!Z^x_11$pU%yw&G zIhYG{t^do7t&K1K>LxIB&NU2heoHh7|69zY;blPm@~0U!K+A!h;(tXIvKG(&>W4JN zfwqTiY`F%lo;;9Lw$Nsg SFUOpS%5Nf&6$12hW0%Zf)V}k3(kPsJg1MoUB4IyaoEny((e)1ToLZ08(+(Yt$ zi |zH;ACw9>Ke*Os!wQ0Z&P?{w*3`22ly0gK@;7=CLtzy3*8X*uR#p}7jr%L5 z`@lVY3Eb0B^M`&P*B^vRE0~J_wiNpEz2?H6(w* zSuOaf{D9r59IzG8Xk*W?mN;|WcJ)&ro8A+cVy^UV`pPOkn-0_-MBh)4zdaHz&S3!D zPA-77d+Xh}92u$!?8@o67%02Px^eI`vFyz1HjmBP#58mC!0cn;9~RY`jy!e09Pa)H zd+~ol!c%Mg7ZP4w @uaxiptaeq8vhh*qRy6+qi}VPSk9_i<_Cd*!3d`f)( zJx}^;1#|BAjr8Nj##T|@-17nxpGWHQAM^zhi0q*!ILj{9!6O3`-86y!Z?=aR)PEy7 zY2V?6M$XoYVc=F{9{eLF`t fhLBD?o5EBz7cL88xOw4})6ZISBO=Srk_GlWJ z(VH@ruAKi9-Q#~RHxVzn# 4yP3sisOUrbAY9!)($8PshpD zdFGk;hE4yB$gODuT%8XXkrDAS%B`2_CR|3CCHia14uqi$fY-^WOPyf*>q=sv8vSDs zJmLQIb&Vn)J&+l*iaodZa2nr$3(rO#zi^(Bwd&WcTdrz+S~ede$6kc^KWPy8vyaQ% zc}&fbTZZO{Y0MzoF~4M|SJWgZ*JFIsNBn21nio(}%R6%Ye&TnEhLU`4? k=|>IsWcz@H_F8PXJTQO>)!=ohu^VQPwH;I6pDX^ z51bg!No%Vc;$;QM!LxE-7Q3C?CJK`}&d~w)_%b<=F~Z*hmre8@8=ZILW|VYg<)-;x zO-=euiB*Fq8jmxThe{uade;sL0GJ3fuM&8=(|=TOJaFmY{TEW>y@9&h+16es!cF`m z9H)#UzkwgWQol6++Rz)yImlCncts*Jk$wsV Isx-_8VDz*Q(nRRPtfoBWN0wMdl?>3_#ru zzpMC?E${1id}qgTt(;_)X!pOm+tGY&Z%qS}zMN5ea|iC;B{PYv&D&3LGj{Ua`fbB+ zuP5nStxK}}7WJXz$)pxa9ee*j1gJazivaZs{v_(30OjCnvu^g*cWOFyUzpOorOvv( zowc%n_ji-96pHuKx!JB@<&cYw>J`qIi!M3WJx5-_L!4;yln;fY+yoEwUvilf^7mJ@ z*O;YT8Gi8%^ogqj)t;_3|9@B>UlB~nfQ@Vu YQ@n%+2?=nW_Zn_}>WTjAgM6d+P1c zY~V`22K(KO)%Je8elzjqFU4h&PU2BS?7eH0{~?7W06vtS32y%+Q@||5Dv_R{s7X-^ zg>I6QCJC&zrw+`#r~svIyk95>?DlRhvoqaH(Hbi0DV;FQd7b`irNOnMcfI+09)N{9 zJqKCp43u8~FIJV?b4Z6b{geNG9tJLX+RJ}|e{=lDFHlueR^+-LMGY_v^1s#6Rn%L+ zE%X?yKJP9Dc)z{#92k|BN$@!s^`^mV{KBEV>k}rirJM@}snHj5yw=`*9M%=16frh- z!rW)w4IEu$e4qr$H4a#~`z;0P^!$^T#;d(=(fQwV*o-_GFE`tMzidi*@z#qkEurAp zK2Do{Ib_v%lgyvFSVYG}Ha^XHP^>$1BY+zsh9ow-XPOvAzosoZdb6da! tNFGTxKz`;H^A-wk0*#XSxk;%j@kC1E<^nbO+EMQ z%hIBpwozr|=i<&nZYNdz{BnRugTz zt2N}D@J%)0m2QkkoP~FcoQencZ!YIc*0=3Gi~s0T-OH(Q6pJr6D?z^Rs`$7DyQT@6 zS64V_^%RnCzPMcXb;B)e%vUBHvCH(t*YWB03tv4)ft ;tjyd~ z(TO!Nbb!S6L@f;eN)~o~y`A|pBRA@~3p8UuFn>|esk^F z@yCl#4$^FDAwh~|`P}5s6U@u=p;i8|fg>C_D@Kwbx%Og;I9QT&Z)wSD`#rugd#_ZR z3^{74A;~gF7KwCyp>#yahh@f^fHy81M_SNdJ ^K0$mdPTRB2iB3xnw<5J_%UhAjvwON=N zU&6LuJ>2*3tOj|s!)Bb}^XBZVX8%}tOESeo0uj(D#!Tc~g#qOa7JX*97925ZTZ2RT z;n_xN2 ivdYug-Mk zq&1+>U_~}P`t6^7Pf5_wHCP_s=LJH;nB7EY`T*gFa(?RgyZX*L=lY8$S+0{pqP!+) zeiZdT>zb}s2ec|&ZpOcGWPhS?!9XG3s5etHLoS|M|K{8~v?7qTnOyW-*NAia?Y3Y) zFY$o|j?IlxrX;cTNTJ0p|74}|E=vvY@=0-~>06#*)TGxdvkQ1N_3L%~gsEiJp_^Ba zobc;}j`XV;NWC{2IpZHb=N8)UkrDC`w{)aerKchN!7m(b39S{TCIO!U8*Vv8^+0WO zMraXFYV25;@poe+(tR$OSgQ7xjN5K6^{q_`x; 90;jc0g=+hhh7#U|^ z$3SN{gtXSvHCYRqtSf7EQwidMLR@LNLQHqapwLI|oVc+|h`hpS#nIR}H&M9@zBBXO z^_wcCFNDf0hkBdYQ%>Q5GY9s5^R>1wP>&EcJy|}fxvAB4QlCqtB#5PwBC4Z_nhp6n z34&WESd$y*hC;uFkD93=bURTJ`gIV(w(rbY!EQ-knM0GI_Gb#JVBDU72P%8A#)&mw z^y$W(bxH=j6j#xJ6@81`;M aeu>JeVda-qzTT% za0;3qtIygg&lDq{dTi2VaphU^)1kNl=fO4M2$Wxb>bprv9}Q8&^vC@B`V4C$RvK31 zDb58Tqf2LlwUciclg;VJDrv6OSj~Mc+?P$By*ej+zv)hLkBq;J75nRMI_an3bf9d0 zNG02Ec2lsSX{;TGkEc;}^^@(-Mg!u%SAW&q@SNe{{w$%RL~egFds%jIlxc?nd!)&E zR&B{}N_*Z~eY`zbeXOqK#W*;nEim4R3!S$*{L=#tk6@60INfVpG-VcCW8G<%QXwn1 zxdw}i&)$>H+=bRH9wJY;l}}1#7RyK{@nMYqo4NF@ktnN#6qkyXZzpO*A99);xOC9SnOq%STJ^>^e3RvoM#eW77gkJ&D#;^{Gaxod)Bmy+y> zGgv81a>Gpw0iEzp^U4#1%?(GN-AgZSI{F0)HX=6j;XoV7Qr!*NHH3z5kZ>v$8!f&I zaZ;UXqji+=qPilvLb LEs5u61Y5#N|f!p0sNCutzw>it}tloLdb zPW-|AG{@5#2sC0WUS2%~9ZcSknccTJ*)Sxvcx|M0eqv+=<58Zo;JGmK?FM~+_uKhj z4El0ck7IfCE0(VMNFW_qVy280`2zKKXgf=Xx)5{rtTW!{C+19=Q?wN0iM7rN;Bp!; z{58B>^Q-WkmItEqxpS S|1J3~*-Y#W}~*Z$7j za2AiMiM3q{evft0XJ#l}ZCQZ~mrm_GRLDzm6I0;uCqAsccuV`G@n6MBYn$tTL=Vyy zMBl!|x%EhnklFfPHH58Bh(`__%WolXIw4x243jk25aPxzaFLYo<`ukuzkYnX4J7co z^3h~f#Yo(=K8l= z`i&*$oC3=akzrw* X=ASTs18;AZeNnReazV3JS-E49wc}C@Nz%yvwR` &y<->0t>+@UqtlOyMa;q!P`1vBZ{w-a+kVp@bt;NJ+lmz|jxHpoi1NG&lpgtR z!tpfQ@#ce){wi3X`F5-;wwBHphH_*3NS;k>rwq2ZdFuQp4G}UQd>$Hf&jro!Oiyhr zc^P$ECrQJLw7bQN%D!jN#+jG?a`@gq>&$MtksehXhkTYTNs}5fbaB!=jm)R0#*fLT zL+RhS%$&OUb!{IIjcGf=s?Lrs9B`{zIhD|&aio2 $muVba0W#I4wxI*k6Q>w3{RS1f7<_!2N?FE39e3ThO?Cc9Zj)dP zYl7f&TBU(} oYPeumDOe%L@m zSny7@oj8wR>&^_x=ficPWXD&|xLocxY#(b`_(Bg$I`4?k)w~Mi^tnYg70&iRz4`U+ z#)v> w-kO{#3+U^x<)VoZLfQBZdGGnkf4 zb`M6v>L3~fcaAHll{iHd7aR}&!c{&e%;9dy3pq5T^}Q>kiwl^aKOBoBU6=#dW9S5X z2$wELq#k1Nl)IA8;%vcwF KgJlz+R?vqI!`?_p1BVSZyP+huc=JtKeRhTA6`pdQ+!v2V)KAM+TM#*$pSk3-% zkkV@C#Flasn){$$p_YBThWBOFO{d85)&{{(#{}9Q*s7`Y%sXeWhmtUHeAzov018u6 z5<|6c`>SjcB!I7}g!mPk)(JzPr#~#>0@IlUvF>_xb6EBcKI5x+ahLv!lzL2VljY>d z6SRCVPNlT9%Q8 L70@{2`u1vWy!7BS8-%Zmz^k6BhUZUI>6hVcO0F@Z^PIS{L?IpHy2guH z+@X`?;Xj7cEvc%Eezu7ci!(`#U!-dB-$zl+F|m1W2fT7Y1_)by)jK%au|JWA9KVMH z$u*(Z_*#gnNeW^6QSK*q$XdUOgd|Dx9IWS7bP%Q;r#;J**@?<;b;(-e-5V&eXRVI) z82ahO-FW2}@EQL_hTU$aGb&)nk)~H(oj=5?pR&&2OI1FQx6;X+U3HhAci)YYATd%< zHE)lJbRcH!S%Wb_hVyL Y%JRl; zP+5Qy^EBej95`-+>~;wsIh)7f2xPN6%Wo(J*eYl7vgi#F72GFuh{1`pNAP%qoAFuL zE <7E8 zbV)uy?AEcHRz;$WKPt)HSuNoHAOb!`?S9J*Wd+(>hmV26wgYJSGQZSf)Nf}6xI zO6L!A_%EK*yWF_Z+?_1}XoM=y>9bAT7bX81{)RoW?LcI9yU2ExjKvwBd+QL;UfqCJ z?0@XenIm={Zl3@3&JT>1R9yLmXv@tGyj}{Mf7*P~h3JL?&8-7+_Lg&h$Fj9a=kyYA z2n^c1AoQZl+~J+(#YBdAX7C=B8PKN=Ckh7J7a@e;Oz+-EpiMJ$a38=#;zDpyK+KXI z0bIZR%v8@e2(WeMxUQKAY2l}&ql~}fKP+Jx3lcdoMAn5-ai3N5E!IIiM9(|nPHKTN zHT WPJ8Rqr%=ci;IeQ=jE>J#;xyNpXnfx<6)46ZKQIhZ@2-v#vVLUC`J zPiovd#lYT*CG-09LpZ*ZBWEDJ>1YeHAO<@%3qC>#cj>{1855B*oMh;5;53zeyN&&U z^!=~|oueQ{?=tFzB9d=f+|l-H6nq4-=O0TJcfwQF4OhkooL{T#N_-srD-av8siRjh zDm1#_-dp}Mn7|!(6;>n(^BL$wT;U;v(5sZ};ncHWnP_}0>%vcchlJcC<(1UPCZ95V z_oGaoiIz~CmjoXztpaE5%G@M5jl5|^U+ 7 Q<+z}c4h`Q?=l VI;dy9H