Cuando el diablo no sabe que hacer, programa en Node

WARNING

Detrás de ese título "provocador" sólo pretendo decir que YO no soy un programador con experiencia en este lenguaje sin entrar a valorar sus ventajas o desventajas. Simplemente se me ocurrió una tontería y quise ponerla en práctica usandolo y así practicar un poco de paso.

Una (si no la única) afición que tengo es la de viajar, y cuanto más lejos y menos visitado sea el sitio mejor. Obviamente con la situación actual de la Covid19 esto es imposible así que lo único que me queda es abrir de vez en cuando Google Maps y seleccionar un sitio al azar (vale, también reviso muchos sitios donde he estado).

Por una asociación de ideas recordé un juego que usa Google Street Maps para mostrarte un sitio random y hay que adivinar donde es en un tiempo corto, simplemente moviendote por las calles sin poder hacer zoom y buscándolo encontré random.earth un site que me fascinó. Muestra luegares random usando imágenes de Google Maps, con la posibilidad de que la gente las vote.

Como ni tengo el tiempo ni la capacidad de hacer algo parecido se me ocurrió hacerme mi versión sencilla y en este post voy a contar cómo lo he hecho (y ya te digo que el código tendrá una calidad regulinchi y ójala una horda de trolls puedan venir a explicarme cómo hacerlo mejor porque así aprenderemos algo)

La idea es simple:

  • tener un comando que ejecutar al iniciar sesión que seleccione unas coordenadas random del planeta

  • construir la url en random.earth con esas coordenadas

  • descargar unos cuantos pantallazos de ese lugar aplicando diferente diferentes zoom desde el más lejano hasta el de máxima resolución

  • crear un gif animado con la secuencia de imágenes

  • subirlo a Mastodon (donde también tengo cuenta como @jagedn)

El resultado final es tener algo parecido a

random place

Al principio jugué con la idea de hacerlo sólo mediante herramientas disponibles en la shell ( curl, httpie, wget, etc ) Asi encontré algunas herramientas en Tk que permiten indicarle una URL y hacen la captura del navegador, trasteé con la shell para generar números random, hice un bash que lo unificaba todo…​. pero al final cambié de opinión y me decidí por hacerlo en un único lenguaje y opté por Node (sí, podía haberlo hecho en Groovy pero no quiero que me acusen de encasillarme)

Instalar Node

Lo primero es instalar Node. No sé si es dificil o no para Windows, pero para Linux no tiene mucha historia.

Básicamente instalar Nvm y con él instalar la versión que nos interese de Node

Proyecto

En un directorio limpio crearemos el proyecto

npm init

Como el proyecto no lo voy a publicar como paquete simplemnte he ido aceptando las opciones por defecto que se ofrecen.

Al final tienes un fichero package.json que puedes luego ajustar a mano si te interesa

Dependencias

Para este proyecto voy a usar las siguientes dependencias:

  • capture-website

  • dotenv

  • get-image-colors

  • gif-creation-service

  • mastodon

$ npm install --save capture-website
$ npm install --save dotenv
$ npm install --save get-image-colors
$ npm install --save gif-creation-service
$ npm install --save mastodon

Index.js

El script va a residir en un único fichero (luego he hecho algunas variantes) index.js que iré exponiendo a continuación por partes:

Inicialización

index.js
const dotenv = require("dotenv");
dotenv.config();

const fs = require("fs");
const captureWebsite = require("capture-website");
const GifCreationService = require("gif-creation-service");
const path = require("path");
const getColors = require("get-image-colors");
const Masto = require("mastodon");

function getRndInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

(async()=>{
    // El código
})();

Básicamente cargamos las dependencias que vamos a usar así como creamos una función de utilidad que parece que Javascript no tiene por defecto.

Vamos a usar la capacidad de Javascript de llamar a funciones de forma asíncrona mediante await así que el código estará embebida en una función de async

Elegir un lugar random

const outputGifFile = "output.gif";
let pngImages = [];

//vamos a buscar un sitio random pero puede ser agua que no interesa
//así que lo intentaremos varias veces hasta encontrar tierra
let doIt = false;
for (var j = 0; j < 20; j++) {

    let lat = getRndInteger(-89, 89);
    let latprec = getRndInteger(0, 9999);
    let log = getRndInteger(0, 179);
    let logprec = getRndInteger(0, 9999);
    let west = getRndInteger(0, 1) == 0 ? 1 : -1;
    log = log * west;

    // tendriamos por ejemplo 32.3430,-113.4350

    // añadimos al array una serie de urls con distinto zoom: ${i}z
    // asociado a un nombre de png
    pngImages = [];
    for (let i = 1; i <= 19; ) {
      pngImages.push([
        `https://random.earth/@${lat}.${latprec},${log}.${logprec},${i}z,2t`,
        `screenshot${i}.png`,
      ]);

      // borramos el fichero si existe
      try {
        fs.unlinkSync(`screenshot${i}.png`);
      } catch (e) {}

      // según el número de steps que usemos tendremos un gif con mayor peso
      i += 3;
    }

    // detectar si es tierra
    ...
}

Detectar si es tierra

Una vez inicializado el array de urls a descargar vamos a descargar la penúltima foto que tiene un un zoom elevado y ver si corresponde a tierra o mar analizando su paleta de colores.

Descargamos la imagen en check.png quitando el elemento HTML #map para que no meta ruido:

await captureWebsite.file(pngImages[pngImages.length - 2][0], "check.png", {element:"#map"});

El elemento #map es un objeto DOM que está en la página propia de random.earth, es decir, que si estás descargando otra página ese elemento no lo tendrás seguramente.

Una vez descargada vamos a analizar su paleta:

doIt = false;
await getColors(path.join(__dirname, "check.png")).then((colors) => {
  if (("" + colors).startsWith("#e3e3dc") == false) {
    doIt = true;
  }
});

La idea es utilizar una librería que nos permite recuperar la paleta de colores de la imagen y ver si nos interesa. A parte de que el código que he usado es muy feo lo que he hecho ha sido comprobar que cuando es mar, la imagen descargada tiene una paleta de colores muy básica que siempre empieza por #e3e3dc, así que básicamente si es diferente he encontrado una imagen interesante.

Al cambiar el valor de doIt a true consigo que se salga del bucle inmediatamente marcando además que hemos encontradao la imagen.

Descargar todas las imagenes

Para descargar las imágenes simplemente llamamos a la librería con el array que habiamos preparado al principio, quitando algunos elementos del DOM para dejar las imágenes lo más limpias posibles

await Promise.all(
    pngImages.map(([url, filename]) => {
      return captureWebsite.file(url, filename, {
		  scaleFactor: 0.75,
		  hideElements: [
            '#address','#search-box','#top-menu','#controls-box','#prev','#next'
        ]
	  });
    })
  );

Generar el Gif

De igual manera, generar el gif es tan sencillo como llamar a la libreria que lo hace:

GifCreationService.createAnimatedGifFromPngImages(
    pngImages.map((obj) => {
      return obj[1];
    }),
    outputGifFile,
    { repeat: true, fps: 1, quality: 10 }
  ).then((outputGifFile) => {
    console.log(
      `Alright, GIF ${outputGifFile} created for ${pngImages[5][0]}!`
    );
  })

Una vez generado el gif muestro con una traza la URL usada para saber el lugar elegido.

Subir a Mastodon

Subir la imagen con un mensaje a Mastodon es tan fácil como haber creado un token de aplicación y apuntar a la instancia donde tienes la cuenta, en mi caso en https://mastodon.madrid

  var M = new Masto({
    access_token: process.env.MASTODON,
    timeout_ms: 60 * 1000,
    api_url: "https://mastodon.madrid/api/v1/",
  });

  var id;
  M.post("media", { file: fs.createReadStream(outputGifFile) }).then(
    (resp) => {
      id = resp.data.id;
      M.post("statuses", {
        status: "Un sitio random cada día",
        media_ids: [id],
      });
    }
  );

Para no tener el token en el código simplemente se crea un fichero .env y se añade como clave-valor

Este código, sube la imagen Gif y una vez subida crea un Toot con un texto y el id de la imagen subida

Ejecución

Una vez tenemos index.js simplemente invocamos

node index.js

y si todo va bien, tendremos un toot subido con un gif animado adjunto

Variantes

Una vez he tenido el script completo he creado dos "variantes":

  • norandom.js, es una copia de index.js pero en lugar de buscar un sitio random utiliza los argumentos proporcionados en la linea de comandos como latitud y longitud

  • social.js, al final he separado en dos la lógica de crear el gif y la de subirlo a Mastodon, de tal forma que primero genero la imagen y si me gusta llamo al social.js para que la suba

Utilidad

Ninguna, pero me he divertido subiendo algún sitio aleatorio y haciendo encuestas para intentar adivinar de donde es, para una vez finalizada la encuesta subir el gif animado que muestre el lugar

Si quieres, puedes descargar el código completo de todos los scripts desde

Conclusión

Mi objetivo con este post NO es crear un tutorial de cómo usar Node, pues como se puede observar ni tengo conocimientos ni el código tiene una calidad mínima.

Digamos que me gustaría sirviera para demostrar que hay una gran cantidad de librerías que hacen las cosas más insospechadas, y que es bastante sencillo crearnos utilidades con ellas.

Follow comments at Telegram groupOr subscribe to the Channel Telegram channel

2019 - 2020 | Mixed with Bootstrap | Baked with JBake v2.6.5