Qué es el “Proyecto Bóveda” con el que EE.UU. quiere asegurar una reserva estratégica de minerales críticos y qué rol juega América Latina.

Qué es el “Proyecto Bóveda” con el que EE.UU. quiere... #qué #es #el #“proyecto #bóveda” #con #que #ee.uu. #quiere #asegurar #una #reserva #estratégica #de #minerales #críticos #y #rol #juega #américa #latina #Politica

https://tardigram.com/m/Politica/t/21677

Esto solo es un script bash para tener una idea de cuantos lectores RSS se tienen.

Introducción

Esto es solo un proyecto rápido originado por la curiosidad que me dio el post de Manuel Moreale sobre cuantos le leen por lectores RSS, el cual también es una reacción a un post de Kev sobre como los lectores leen sus contenidos. Ambos han recurrido a formularios y han obtenido sus resultados, siendo que Manuel Moreale incluso añadió datos técnicos sacados de sus Logs, con lo que tuve la idea de evitar los formularios e ir directo a los Logs. Entiendo que hago esto desde una posición de privilegio del conocimiento. Después de todo, tengo acceso a mis logs y puedo procesarlos a gusto. Compartiré el código con el fin de que otros puedan usarlo y si asi lo desean, contribuir con ideas para mejorarlo, pero eso también beneficiaria solo a los que tengan acceso a sus Logs. Tampoco es para tanto. Si sale bien, se me podría ocurrir una idea para que sea mas fácil de utilizar. Los logs de nginx/apache son abrumadores. Ya antes tuve que segmentarlos para no perderme entre tanta cosa, separando los logs por subdominios. Aun así, la cantidad de trafico kk es abrumadora. Hay demasiados bots, crawlers y personas con intenciones sospechosas, y leerlos da flojera. Es demasiado que analizar. Desde hace rato, he estado intentando de entender comandos de linux que ayuden a procesar texto. La verdad, las herramientas existentes, aunque cripticas, son asombrosas. Mira la velocidad en la que trabajan con grandes volúmenes de datos y el resultado siempre es excelente. Me salvé gracias a esto para segmentar archivos SQL y ahora lo aprovecharé para este breve experimento.

Descripción del Problema

En teoría, suena sencillo. Solo necesito recopilar los datos de la URL /feed que es donde publica mi blog de WordPress los Feeds RSS. En teoría, solo los lectores RSS deberían visitar esa URL pero ya sabes, ahi estan metidos los bots y crawlers todo el tiempo, asi que es muy fácil inflar las estadísticas con datos que no queremos. También, aunque se ejecute el script de forma regular, corre el riesgo de captar datos repetidos inflando aun mas las estadísticas, así que, es necesario evitar datos repetidos. Para saber cuantos lectores RSS unicos visitan el sitio, lo mejor es hacer una asociacion entre la direccion IP y su User Agent de esta manera puedo generar un UID que permitiria sumar la estadistica de cuantos lectores RSS estan cargando mi contenido desde el Feed. Esto por supuesto, no es infalible. especialmente los Feeds que cambian la version en el nombre del User Agent, quedarian sumados como un lector adicional en lugar de ser unico. Otra cosa es que es difícil saber cual es un lector RSS y cual no. Como criterio de búsqueda, he determinado que este tenga las palabras RSS, Feed, Reader, SimplePie, W3C_Validator, TinyPRSS y NewsBlur en su user agent. Iré sumando ideas, pero hay demasiados. a veces pienso que hay mas lectores de RSS que blogs. Como curiosidad, también agregué patrones para identificar lectores del fediverso (teóricamente algunas plataformas como friendica, pueden seguir un sitio mediante los RSS) así que agregue los patrones de Mastodon, Pleroma, Friendica, PixelFed, Lemmy y ActivityPub. Por ultimo, bots. Todo lo que parezca bot (asumiendo que son honestos y dicen ser bots) con los patrones bot, googlebot, bingbot, slurp, duckduckbot, yandexbot, spider, crawl. Para este proceso hay que hacer algunos archivos temporales que serviran para el procesamiento de los datos.

Control-RSS

El script es sencillo en teoria. Tiene un monton de Regex, pero solo eso es lo unico intimidante. De hecho te animo a ayudarme a mejorarlo, aunque no se tampoco que interes habria en mejorarlo. Igual, Siguiendo el patrón que he aprendido al hacer el bot de Channel2RSS en el que se separa la configuración del programa, tengo un archivo de configuración, el cual debe llamarse config.conf, pero que dejo un ejemplo que añade la extensión .example. # --- Configuración del Script ---LOG_FILE="/var/log/nginx/access.log" # Ruta a tu archivo de log principal# Si usas Apache, podría ser /var/log/apache2/access.log o similarURL_A_MONITORIZAR="/feed" # El script es principalmente para ver datos de RSS, pero lo puedes modificar tu gustoOUTPUT_FILE="visitantes_unicos.json" # Archivo JSON de salida# --- Fin de Configuración --- Antes de ejecutar el programa, debes asegurarte de que tienes instalado JQ, GnuPlot y AWK. JQ sirve para procesar JSON en la consola, GnuPlot para los graficos y AWK para el procesamiento de texto plano en los logs. #!/bin/bash# 1. Cargar configuraciónif [ ! -f config.conf ]; then echo "Error: config.conf no encontrado"; exit 1; fisource ./config.conf # Inicializar JSON si no existeif [ ! -f "$OUTPUT_FILE" ]; then echo '{"datos": [], "resumen": {}}' > "$OUTPUT_FILE"fiecho "Procesando logs de $LOG_FILE..."# 2. Extracción rápida con AWK awk -v url="$URL_A_MONITORIZAR" '$7 == url || $7 ~ url { split($4, t, /[:/]/); fecha=t[1]"/"t[2]"/"t[3]; hora=t[4]":"t[5]; ua=""; for(i=12; i<=NF; i++) ua=(ua=="" ? $i : ua" "$i); gsub(/"/, "", ua); print $1 "|" fecha "|" hora "|" ua}' "$LOG_FILE" | sort -u > temp_data.txt # 3. Actualización Incremental del JSONecho "Actualizando base de datos (evitando duplicados)..."while IFS="|" read -r ip fecha hora ua; do # ID Único: Asociación IP + Agente ID_UNICO=$(echo "${ip}${ua}" | md5sum | cut -d' ' -f1) # Clasificación de Agentes IS_RSS=$([[ "$ua" =~ (RSS|Feed|Reader|SimplePie|W3C_Validator|TinyPRSS|NewsBlur) ]] && echo "true" || echo "false") IS_FEDIVERSE=$([[ "$ua" =~ (Mastodon|Pleroma|Friendica|PixelFed|Lemmy|ActivityPub) ]] && echo "true" || echo "false") IS_BOT=$([[ "$ua" =~ (bot|googlebot|bingbot|slurp|duckduckbot|yandexbot|spider|crawl) ]] && echo "true" || echo "false") # Inserción segura con JQ (Solo si el UID no existe) jq --arg id "$ID_UNICO" --arg ip "$ip" --arg f "$fecha" --arg h "$hora" --arg ua "$ua" \ --arg rss "$IS_RSS" --arg fedi "$IS_FEDIVERSE" --arg bot "$IS_BOT" \ 'if .datos | any(.[]; .uid == $id) then . else .datos += [{"uid": $id, "ip": $ip, "fecha": $f, "hora": $h, "agente": $ua, "es_rss": ($rss=="true"), "es_fedi": ($fedi=="true"), "es_bot": ($bot=="true")}] end' \ "$OUTPUT_FILE" > "$OUTPUT_FILE.tmp" && mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE"done < temp_data.txt # 4. Cálculo del Resumen (Corregido error de sintaxis)AHORA=$(date +"%Y-%m-%d %H:%M:%S") jq --arg fecha_act "$AHORA" '.resumen = { "total_visitantes_unicos": (.datos | length), "usuarios_rss": ([.datos[] | select(.es_rss == true)] | length), "usuarios_fediverso": ([.datos[] | select(.es_fedi == true)] | length), "bots_crawlers": ([.datos[] | select(.es_bot == true)] | length), "usuarios_estandar": ([.datos[] | select(.es_rss == false and .es_fedi == false and .es_bot == false)] | length), "ultima_actualizacion": $fecha_act}' "$OUTPUT_FILE" > "$OUTPUT_FILE.tmp" && mv "$OUTPUT_FILE.tmp" "$OUTPUT_FILE"# 5. Generar Gráfica de Frecuencia jq -r '.datos[].fecha' "$OUTPUT_FILE" | sort | uniq -c | awk '{print $2, $1}' > freq.dat gnuplot <<EOFset terminal pngcairo size 800,400set output 'grafica_semanal.png'set title "Evolución de Visitantes Únicos"set xtics rotate by -45set style fill solidplot 'freq.dat' using 2:xtic(1) with boxes title "Visitas"EOF# 6. Generar Informe HTML cat <<HTML > informe.html<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Análisis de Audiencia 2026</title> <style>body{font-family:sans-serif; margin:40px; background:#f4f4f9;} .card{background:white; padding:20px; border-radius:8px; box-shadow:0 2px 5px rgba(0,0,0,0.1);}</style></head><body> <div class="card"> <h1>Informe: $URL_A_MONITORIZAR</h1> <p><b>Actualizado:</b> $(jq -r '.resumen.ultima_actualizacion' "$OUTPUT_FILE")</p> <hr> <ul> <li>👤 <b>Visitantes Únicos:</b> $(jq '.resumen.total_visitantes_unicos' "$OUTPUT_FILE")</li> <li>rss <b>Lectores RSS:</b> $(jq '.resumen.usuarios_rss' "$OUTPUT_FILE")</li> <li>🌐 <b>Fediverso (Mastodon/Etc):</b> $(jq '.resumen.usuarios_fediverso' "$OUTPUT_FILE")</li> <li>🤖 <b>Bots y Crawlers:</b> $(jq '.resumen.bots_crawlers' "$OUTPUT_FILE")</li> <li>🖥️ <b>Navegadores Estándar:</b> $(jq '.resumen.usuarios_estandar' "$OUTPUT_FILE")</li> </ul> <img src="grafica_semanal.png" style="max-width:100%;"> </div></body></html>HTML rm temp_data.txt freq.dat echo "Proceso completado. Revisa informe.html y $OUTPUT_FILE" De forma muy resumida, este código permite ir creando un archivo JSON que puedes usar a tu gusto de manera privada. Recuerda tener cuidado con esto pues contiene las IP de las personas que te siguen y probablemente ni ti ni ellos querrán que se divulgue esa información. Basado en este archivo JSON se genera una imagen con GNUPlot mediante simplemente contar las entradas por fechas. Por ultimo, Genera un archivo HTML que te permitirá ver de forma resumida visitantes unicos, lectores de RSS, Fediverso, Bots y Crawlers y Navegadores Estandares. Puedes poner estos dos últimos, la imagen y el archivo HTML, en un directorio publico de tu servidor, en mi caso como hice en el post de NextCloud y Aria2 usando NGinx para tener un directorio que puedo usar de forma publica sin que WordPress lo neutralice. En mi caso, estoy pensando en hacer una página para mostrar esta información, pero a modo de ejemplo, la dejaré aquí, incrustada como iframe como hice en la entrada de las galerías especiales. 

Conclusiones

El experimento es interesante. RSS es solo un archivo y no se le suele prestar mucha atención, así que en las estadísticas no suele contar cuantos lo utilizan y visto lo visto, es relativamente difícil sacar resultados apreciables a la hora de saber cuantos individuos lo usan para acceder al sitio. Para este ejercicio he intentado asociar la dirección IP con el user agent, con lo que se puede hacer una idea, pero la misma persona puede usar diferentes lectores en diferentes dispositivos, así sea la misma IP. He dejado este código en publico por si le interesa a alguien. Por supuesto, le he creado una entrada en mi repo git para poder clonarlo y usarlo por cualquiera que así lo desee. https://git.interlan.ec/Drk0027/control-rss ¿Deseas hacer algun comentario? Aqui hay una caja de comentarios que puedes utilizar. ¿Quieres contactarte conmigo? Por correo electronico por supuesto en [email protected] ¿Quieres profundizar en este tema? Vamos pal foro en forum.interlan.ec https://interlan.ec/blog/2026/03/06/proyecto-control-rss/ #Código #DIY #experimentos #linux #logs #nginx #programacion #proyecto #recursos #selfhosting #servidores

--- Eterniun Legs ---

#Proyect #Concept #tfp #Photo #Art #Bizkaia #Nikon #Legs #Fineart

*** Los caminos, solo nos conducen

#Portait #Model #Woman #photography #portraitphotography

*** Tú decides como transitas por ellos...

#Foto #Proyecto #Conceptual #Persona #Arte #Studio #black #nailon

Un proyecto europeo investigará cómo ciertos virus convierten nuestras células en “factorías virales”

Un proyecto europeo investigará cómo ciertos virus... #proyecto #europeo #investigará #cómo #ciertos #virus #convierten #nuestras #células #en #“factorías #virales #Ciencia

https://tardigram.com/m/Ciencia/t/20320

Un proyecto europeo investigará cómo ciertos virus convierten nuestras células en “factorías virales” - Ciencia - Tardigram

Un equipo mixto, con participación Consejo Superior de Investigaciones Científicas (CSIC), formará parte de un nuevo proyecto europeo que busca entender cómo algunos virus son capaces de tomar el control de células humanas y transformarlas en factorías virales, estructuras donde se producen y ensamblan nuevos virus. La...

--- La cultura es ... ---
#Proyect #Cocept #tfp #Photo #Art #Nikon #Skin #Antigua #Intima
*** Sin cultura el mundo es inerte
#Portait #Model #Woman #photography #girl #portraitphotography
*** Sin cultura no se sabe que es la belleza
#Foto #Proyecto #Conceptual #Persona #LLodio #house #Mujer #Piel
Bienvenidos a la librería

Si llegaste aquí estás más perdido que en los backrooms. Descripción y motivaciones del sitio.

La Biblioteca de Fando

Fairwork

Tras nueve meses de intenso trabajo colaborativo entre personas investigadoras de ocho países de América Latina, la red Fairwork en la región se complace en anunciar el lanzamiento simultáneo de siete reportes que aplican la metodología Fairwork en el contexto latinoamericano, junto con la publicación de la versión en español del reporte Fairwork Brasil, originalmente difundido en portugués e inglés en septiembre del año pasado.

https://www.tedic.org/fairwork-2/

#Proyecto

Coronado por el logo mas feo que he hecho a partir de una tijera y un altavoz, aquí estamos de nuevo con un ensayo de PWA.

Introducción

No es que no hayan alternativas ni pienso superar lo que ya existe. El problema es que hay muchísimas alternativas, o muy amplias para una sola tarea, o especificas, pero rellena de publicidad. Este proyecto es una excusa para tener un cortador de audio simple y estudiar aplicaciones web progresivas. Un win-win, ¿no crees? Por supuesto, la aplicación quedará documentada y servida, con la opción de descargar desde mi repo. Analizaremos como fue desarrollada y bajo que principios, sirviendo como un proyecto mas para mi estantería.

El problema

Necesitaba un cortador simple y sin publicidad. Asi, sin rodeos. Pensé en un inicio en usar simplemente FFMPEG para esta tarea, pero también quería que sea portable. FFMPEG funciona bien para linux, windows y mas plataformas, pero esta tarea era muy concreta. Esto me hizo descartar una implementación en webassembly, que le permite funcionar de forma nativa en un navegador. En los primeros ensayos funcionó de maravilla, pero iba en contra del principio de minimalismo que buscaba. Aparte claro, de que agregaba carga al procesador y memoria que no eran necesarias. https://github.com/ffmpegwasm/ffmpeg.wasm En principio, el aprovechamiento de los archivos de audio en los navegadores modernos es algo que se puede usar de forma nativa. Se pueden usar formatos como mp3, mp4, ogg, wav, etc, pero para exportar, solo se puede entregar el archivo en formato WAV, esto es algo que analizaremos posteriormente. Como no quiero todavia involucrarme en el desarrollo android y queria hacerlo multiplataforma, pense en usar flutter. Tras unos cuantos fracasos por inexperiencia y porque flutter tiene limitaciones que me sorprendieron, volvi a buscar una alternativa. El resultado es que el desarrollo funcional actual está en una aplicacion PWA universal.

El desarrollo

Estos son los requisitos:
  • Multiplataforma. Que funcione en todo dispositivo posible.
  • Minimo codigo. Con la menor cantidad posible(para mi XD) de codigo. Utilizo librerias como waveform, pero estaba pensando en hacerlo aun mas minimalista
  • Integracion en la menor cantidad de archivos. CSS y JS en el mismo HTML
  • Tecnologias nativas. Solo funciones integradas en el navegador.
  • Actualizable. Aunque no se que cambios podria hacerle.
  • Distribuible. Se lo puede separar de mi servidor y empaquetar para otras plataformas.
  • Offline. Que no dependa de mi servidor para funcionar, excepto para la distribucion y actualizacion.
  • Codigo libre. Dudo que le interese a alguien algo tan simple, pero ta. ahi ta el codigo. Profanalo como quieras pero recuerda mencionar (MIT)
  • Tras unos cuantos bocetos, la primera opcion que parecia adecuada, era Web Audio API. De forma sencilla y sin mucho lio, permite utilizar los formatos de audio de entrada nativos del navegador, pero solo permite salir el formato WAV. Es la forma mas rapida que hay, pero la menos flexible. Dado que no quiero depender de demasiados complementos externos, es mas rapido de utilizar que el ffmpeg wasm. Por supuesto, tambien para dar un poco de estilo, utilicé wavesurfer, para una representacion grafica del archivo de audio. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119let ws, wsRegions; const audioInput = document.getElementById('audioInput'); const btnPlay = document.getElementById('btnPlay'); const btnExport = document.getElementById('btnExport'); // Inicializar Wavesurfer ws = WaveSurfer.create({ container: '#waveform', waveColor: '#4F4A85', progressColor: '#383351', responsive: true, }); // Plugin de regiones (para seleccionar el área) wsRegions = ws.registerPlugin(WaveSurfer.Regions.create()); audioInput.onchange = (e) => { const file = e.target.files[0]; if (file) { const url = URL.createObjectURL(file); ws.load(url); } }; ws.on('ready', () => { wsRegions.clearRegions(); wsRegions.addRegion({ start: 0, end: ws.getDuration() / 4, color: 'rgba(0, 255, 0, 0.3)', drag: true, resize: true }); }); btnPlay.onclick = () => { const region = Object.values(wsRegions.getRegions())[0]; if (region) region.play(); }; btnExport.onclick = async () => { const region = Object.values(wsRegions.getRegions())[0]; if (!region) return alert("Selecciona un área primero"); const originalBuffer = ws.getDecodedData(); const start = region.start; const end = region.end; const segmentBuffer = cutAudio(originalBuffer, start, end); downloadAudio(segmentBuffer); }; function cutAudio(buffer, start, end) { const sampleRate = buffer.sampleRate; const frameCount = (end - start) * sampleRate; const newBuffer = new AudioContext().createBuffer(buffer.numberOfChannels, frameCount, sampleRate); for (let i = 0; i < buffer.numberOfChannels; i++) { const channelData = buffer.getChannelData(i).slice(start * sampleRate, end * sampleRate); newBuffer.copyToChannel(channelData, i); } return newBuffer; } function downloadAudio(buffer) { // Conversión simple a WAV para exportación rápida const wavData = bufferToWav(buffer); const blob = new Blob([wavData], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = "corte_audio.wav"; anchor.click(); } // Helper para convertir AudioBuffer a formato WAV function bufferToWav(buffer) { let numOfChan = buffer.numberOfChannels, length = buffer.length * numOfChan * 2 + 44, bufferArr = new ArrayBuffer(length), view = new DataView(bufferArr), channels = [], i, sample, offset = 0, pos = 0; function setUint16(data) { view.setUint16(pos, data, true); pos += 2; } function setUint32(data) { view.setUint32(pos, data, true); pos += 4; } setUint32(0x46464952); // "RIFF" setUint32(length - 8); // file length setUint32(0x45564157); // "WAVE" setUint32(0x20746d66); // "fmt " chunk setUint32(16); // length = 16 setUint16(1); // PCM (uncompressed) setUint16(numOfChan); setUint32(buffer.sampleRate); setUint32(buffer.sampleRate * 2 * numOfChan); // avg. bytes/sec setUint16(numOfChan * 2); // block-align setUint16(16); // 16-bit setUint32(0x61746164); // "data" chunk setUint32(length - pos - 4); // chunk length for(i=0; i<buffer.numberOfChannels; i++) channels.push(buffer.getChannelData(i)); while(pos < length) { for(i=0; i<numOfChan; i++) { // interleave channels sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = (sample < 0 ? sample * 0x8000 : sample * 0x7FFF); view.setInt16(pos, sample, true); pos += 2; } offset++; } return bufferArr; } // Registro del Service Worker para PWA if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js'); } Dado que me interesa que el formato de entrada, como minimo sea igual al de salida, hice este cambio. A diferencia del método anterior que reconstruía el audio bit por bit, el MediaRecorder captura el flujo de audio mientras se procesa y lo empaqueta en el formato que el navegador soporta nativamente (usualmente el mismo que el de entrada). El codigo resultante por supuesto, tambien es mas compacto. let ws, wsRegions, activeRegion, lastBlob, originalFileType; const audioInput = document.getElementById('audioInput'); const status = document.getElementById('status'); ws = WaveSurfer.create({ container: '#waveform', waveColor: '#4f46e5', progressColor: '#818cf8', height: 120 }); wsRegions = ws.registerPlugin(WaveSurfer.Regions.create()); audioInput.onchange = (e) => { const file = e.target.files[0]; if (file) { originalFileType = file.type; // Guardamos el formato original ws.load(URL.createObjectURL(file)); document.getElementById('btnShare').style.display = 'none'; } }; ws.on('ready', () => { const duration = ws.getDuration(); document.getElementById('totalTime').innerText = `Total: ${formatTime(duration)}`; wsRegions.clearRegions(); activeRegion = wsRegions.addRegion({ start: 0, end: Math.min(duration, 10), color: 'rgba(59, 130, 246, 0.3)', drag: true, resize: true }); updateSelectionLabel(); }); wsRegions.on('region-updated', updateSelectionLabel); function updateSelectionLabel() { if (activeRegion) { document.getElementById('selectionTime').innerText = `Selección: ${formatTime(activeRegion.start)} - ${formatTime(activeRegion.end)}`; } } function formatTime(s) { return new Date(s * 1000).toISOString().substr(14, 5); } document.getElementById('btnPlay').onclick = () => activeRegion && activeRegion.play(); document.getElementById('btnStop').onclick = () => ws.stop(); // EXPORTACIÓN CON MEDIARECORDER (Mantiene formato de origen/comprimido) document.getElementById('btnExport').onclick = async () => { if (!activeRegion) return; status.innerText = "Procesando..."; const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const sourceBuffer = ws.getDecodedData(); const duration = activeRegion.end - activeRegion.start; const offlineCtx = new OfflineAudioContext( sourceBuffer.numberOfChannels, duration * sourceBuffer.sampleRate, sourceBuffer.sampleRate ); const source = offlineCtx.createBufferSource(); source.buffer = sourceBuffer; source.connect(offlineCtx.destination); source.start(0, activeRegion.start, duration); const renderedBuffer = await offlineCtx.startRendering(); // Grabación del Stream para comprimir const destination = audioCtx.createMediaStreamDestination(); const recorder = new MediaRecorder(destination.stream); const chunks = []; recorder.ondataavailable = (e) => chunks.push(e.data); recorder.onstop = () => { lastBlob = new Blob(chunks, { type: originalFileType || 'audio/mp4' }); const url = URL.createObjectURL(lastBlob); const a = document.createElement('a'); a.href = url; a.download = `cut_${audioInput.files[0].name}`; a.click(); status.innerText = "¡Exportado!"; document.getElementById('btnShare').style.display = 'inline-block'; }; const playSource = audioCtx.createBufferSource(); playSource.buffer = renderedBuffer; playSource.connect(destination); recorder.start(); playSource.start(); playSource.onended = () => recorder.stop(); }; // COMPARTIR (Botón 3 bolitas nativo Android) document.getElementById('btnShare').onclick = async () => { if (!lastBlob) return; const file = new File([lastBlob], `cut_${audioInput.files[0].name}`, { type: lastBlob.type }); if (navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: 'Audio Cortado', text: 'Compartido desde Audio Cutter PWA' }); } }; Con esto, el codigo ya es funcional incluso al abrirlo como archivo local. Podrias copiar el codigo a tu compu y no necesitarias de un servidor para generar localhost.

    Convertir a PWA

    una vez listo el desarrollo, queda apuntar al proposito del proyecto; que sea una App offline. Pero como sabras, javascript es un lenguaje para la web, asi que tenemos que aprovechar una tecnologia ampliamente aceptada (aun no por todos) para asegurar el funcionamiento offline; PWA. Con PWA puedes hacer software que funciona como si fuera una app nativa. Claro, necesitas preparativos previos, pero principalmente necesitas un archivo manifest.json y un archivo sw.js que, registrados en tu app javascript, le indican al navegador lo que necesitamos para que funcione offline. Todo lo que haremos a continuacion puede realizarse asistidos por la pagina PWABuilder Primero, necesitamos un manifest.json { "name": "Audio Cutter PWA", "short_name": "Cutter", "start_url": "index.html", "display": "standalone", "background_color": "#121212", "theme_color": "#4A90E2", "description": "Simple recortador de audio para PWA. Hecho por Drk0027", "share_target": { "action": "/index.html", "method": "POST", "enctype": "multipart/form-data", "params": { "files": [ { "name": "audio_file", "accept": [ "audio/*" ] } ] } }, "user_preferences": { "color_scheme_dark": { "theme_color": "#121212", "background_color": "#121212" } }, "screenshots" : [ { "src": "screenshot.png", "sizes": "740x383", "type": "image/png", "platform": "wide" } ], "orientation": "any", "id": "audiocutter", "categories": ["utilities", "music"] } El archivo manifest.json le permite al navegador que caracteristicas tiene la app. PWABuilder tiene listada una amplia cantidad de funciones casi nativas que permite PWA, por lo que no es mala idea hacer un desarrollo en este sistema si no necesitas trabajar a bajo nivel. Lo siguiente que hay que hacer es el archivo sw.js Este archivo permite al navegador saber que es lo que se mantendrá offline y la forma en la que se actualizara en caso de cambios en el servidor. const CACHE_NAME = 'v1_cache_mi_pwa'; // Lista de archivos que quieres que funcionen sin internetconst urlsToCache = [ '/', '/index.html', '/plugins/regions.min.js', '/wavesurfer.min.js' ]; // 1. Evento Install: Guarda los archivos en la caché al instalar la PWA self.addEventListener('install', e => { e.waitUntil( caches.open(CACHE_NAME) .then(cache => { return cache.addAll(urlsToCache); }) .then(() => self.skipWaiting()) // Fuerza la activación inmediata ); }); // 2. Evento Activate: Limpia cachés antiguas para que siempre tengas la última versión self.addEventListener('activate', e => { const cacheWhitelist = [CACHE_NAME]; e.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); // 3. Evento Fetch: Intercepta las peticiones y sirve desde la caché si existe self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request) .then(res => { if (res) { // Si el archivo está en caché, lo devuelve sin ir a internet return res; } // Si no está, lo busca en la red return fetch(e.request); }) ); }); self.addEventListener('fetch', (e) => { e.respondWith(caches.match(e.request).then(res => res || fetch(e.request))); }); La magia de PWA es que una vez instalado, no necesitas descargar nada. Todo se actualiza de forma transparente y no necesitas atormentar al cliente. Por supuesto, es una buena practica avisar al cliente si actualizas algo, asi que he agregado esa funcion tambien en este ejemplo. Hay que registrar en el sw.jscada archivo que quieres cachear para mantener offline. Yo por pereza solo puse unos cuantos, pero se recomienda que sean todos. Hay maneras mas automaticas de lograrlo, pero eso es tarea para una proxima clase. Por ultimo, necesitamos registrar en el index el archivo manifest.jsony el service worker de la siguiente manera. <link rel="manifest" href="manifest.json"> <script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js') .then(reg => console.log('Registro exitoso', reg)) .catch(err => console.warn('Error al registrar', err)); } if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(reg => { reg.onupdatefound = () => { const installingWorker = reg.installing; installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // Aquí es donde le avisas al usuario alert('Nueva versión disponible. Por favor, recarga la página.'); } } }; }; }); } if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js').then(reg => { // Detecta si hay una actualización esperando reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { // Cuando el nuevo SW se ha instalado completamente if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // Crea una confirmación para el usuario const r = confirm("¡Hay una nueva versión disponible! ¿Quieres actualizar ahora?"); if (r === true) { window.location.reload(); // Recarga la página para aplicar cambios } } }); }); }); }); } </script> De paso agregamos un detector que permitira actualizar los archivos cacheados por si hacemos alguna mejora a la app desde el servidor.

    El despliegue para otras plataformas

    En realidad no es taaan necesario, pero igual lo hago. Esto para probar las funciones que ofrece PWA de forma nativa, que entre otras cosas, incuye funciones del boton de compartir, y tambien de «abrir con». Por el momento, y a modo de prueba para obtanium, he agregado al repositorio publico el directorio de release, con el fin de comprobar si es posible utilizarlo de esta manera. Puedes encontrarlo en esta url si te interesa. https://git.interlan.ec/Drk0027/audio-cutter-pwa/releases El APK fue creado usando PWABuilder. Tras ajustar todos los requerimientos, me dió los archivos necesarios para el despliegue multiplataforma. Deja incluso firmando el APK. Como novedad y aprendisaje, resulta que obtanium no obtiene desde una carpeta release especifica, sino que tiene un apartado llamado lanzamientos, por lo que poner en el repo los releases es innecesario, en proximas revisiones eliminaré los archivos innecesarios. Al exportar con la pagina, me dio este log, que me daba la impresion de que fallaba por el tiempo que tomo. No fallaba, si que toma tiempo jajaja. Querying for job…2026-01-30T02:04:25.557Z [info]: Generating app package for https://audio-cutter-pwa.interlan.ec2026-01-30T02:04:25.558Z [info]: Creating temp directory…2026-01-30T02:04:25.564Z [info]: Creating signing information…2026-01-30T02:04:25.565Z [info]: Using Bubblewrap to generate app package…2026-01-30T02:04:25.566Z [info]: Creating Trusted Web Activity (TWA) project…2026-01-30T02:04:27.965Z [info]: Building the app package with Gradle. This can take a few minutes…2026-01-30T02:04:45.382Z [info]: Signing APK…2026-01-30T02:04:50.621Z [info]: Signing the app package…2026-01-30T02:04:55.960Z [info]: App package signed successfully2026-01-30T02:04:55.960Z [info]: Generating asset links…2026-01-30T02:05:00.726Z [info]: Digital Asset Links file generated at /tmp/pwabuilder-cloudapk–19-eBPgGJdJxsEV/app/build/outputs/apk/release/assetlinks.json2026-01-30T02:05:00.726Z [info]: Building App Bundle…2026-01-30T02:05:00.726Z [info]: Generating app bundle2026-01-30T02:05:13.770Z [info]: App bundle built successfully.2026-01-30T02:05:13.770Z [info]: Successfully created app package for https://audio-cutter-pwa.interlan.ec2026-01-30T02:05:13.771Z [info]: Process completed in 48 seconds2026-01-30T02:05:13.771Z [info]: Zipping app package…2026-01-30T02:05:15.965Z [info]: App package zipped successfully.2026-01-30T02:05:15.965Z [info]: Successfully generated Google Play package. Saving zip file…2026-01-30T02:05:16.492Z [info]: Successfully uploaded package zip file [«-audio-cutter-pwainterlanec-409230zip»]Package created successfully. Download has begun.

    Observaciones del resultado

    La aplicacion convertida a APK para android no se ve especialmente nativa. Tambien es cierto que elegi una interfaz mas apta para una pantalla grande de computadora, por lo que da la impresion de ser muy estrecho. Aun asi, es utilizable. funciona muy bien y no es incomodo en la interfaz tactil. Eso si, tengo que hacer mas pruebas para evaluar el rendimiento, pero todo apunta a que es mejor volver a la exportacion mediante WAV, debido a que el resultado es muy variable. En el telefono de pruebas, un Infinix Smart 8, bastante basico, tardo casi 5 minutos en generar el resultado y la calidad de sonido es bastante baja. Pero en una computadora, la salida tiene una calidad similar a la de origen. PWA-audio-cutter Por otra parte, entre las funciones nativas que ofrece PWA es la opcion de «Abrir con» que permite utilizar los elementos nativos de android para elegir la aplicacion con la que se quiere abrir una lista limitada de archivos, en este caso, archivos de audio. pantalla de abrir con, para elegir pwa-audio-cutter como programa con el que editar el audio

    Por supuesto, no es funcional puesto que solo lo hice para probar el resultado, pues PWABuilder ofrecia esta capacidad. Para proximas versiones podria aprender a utilizarla.

    Conclusiones

    Mis experimentos con PWA no son recientes, ya antes he probado hacerlo con el juego de la vida y el SSB y por supuesto, no han sido tan prosperos como este pues nunca me atrevi a utilizar el PWABuilder. Actualmente podrian funcionar de esa manera, podria ponerlos en esa pagina y podrian ser funcionales o les faltaria algo que deba cambiar para que trabaje, no lo se, debo revisar. Pero es muy interesante en el sentido de que este camino se puede tomar para hacer aplicaciones universales bastante cercanas a funcionalidad a aplicaciones nativas. Por supuesto, hay limites y tambien es que javascript es un lenguaje muy para la web. Aunque la app sea offline, la necesidad de llamada a APIS hace que realmente no haya tanta practicidad a la hora de utilizarlas. A menos que se haga un desarrollo mas pesado en el que se guarden recursos localmente y que se actualicen al volver a tener internet. Me gustaria ver si es posible hacer un juego con PWA y Godot. O solo hacer un juego en godot. Eso es otra cosa que sigo teniendo pendiente porque aunque trabajo mucho en temas de computacion e informatica, tambien soy un artista. Seguro que se nota mas cuando empiece a poblar mi otro blog jajaja. https://interlan.ec/blog/2026/02/20/proyecto-super-simple-audio-cutter/ #android #audioCutter #Código #DIY #ffmpeg #mp3 #openSource #proyecto #PWA #status #timeDisplay #waveform