shorturl

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

En el vertiginoso mundo digital, compartir enlaces largos y complejos puede resultar engorroso. Es en este escenario donde Tecnops URL Shortener, una herramienta desarrollada con tecnologías tan conocidas y usadas como Node.js, Express, JavaScript, CSS y HTML, se presenta como la solución ideal para simplificar y optimizar tus enlaces, ofreciéndote una experiencia de navegación más eficiente.

Creando una URL Corta Personalizada

  1. Accede a la plataforma Tecnops URL Shortener visitando //tecnops.es:10000 en tu navegador favorito.
  2. En el campo URL, introduce la URL que deseas acortar.
  3. Asigna un alias único y fácil de recordar en el campo Alias. Por ejemplo, podrías utilizar mi-alias.
  4. Haz clic en el botón Acortar para generar tu URL corta personalizada con el alias.

crear url

Accediendo a tu URL Acortada

Para acceder a la URL acortada que has creado, sigue estos simples pasos:

  1. Utiliza el siguiente formato de URL: //tecnops.es:10000/code/mi-alias.
  2. Reemplaza mi-alias con el alias que asignaste al acortar la URL.

Al abrir esta URL en tu navegador, experimentarás una redirección automática hacia la URL original asociada con el alias.

Este proceso, respaldado por las tecnologías líderes de Node.js, Express, JavaScript, CSS y HTML, hace que compartir enlaces sea más sencillo y elegante, permitiéndote mantener el control sobre tus enlaces y mejorar la experiencia de tus usuarios.

Beneficios de Tecnops URL Shortener:

  • Personalización: Asigna alias significativos para tus enlaces y crea una identidad única.
  • Gestión Sencilla: Accede a una interfaz intuitiva para crear tus enlaces acortados.
  • Eficiencia: Comparte enlaces de manera efectiva, ya sea en redes sociales, correos electrónicos o mensajes.

Conclusión: Tecnops URL Shortener, construido con tecnologías avanzadas, redefine la forma en que interactuamos con los enlaces en línea. ¡Simplifica tu experiencia en la web hoy mismo y descubre el poder de tus enlaces acortados con Tecnops!

totp-2fa

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

En esta nueva entrada vamos a realizar un sistema de autenticación de doble factor  (2FA) usando Node.js en el Back End y Javascript en el Front End.

Bien, antes de empezar a codificar vamos a definir dos conceptos que son la base de este tutorial: 2FA y TOTP.

Las siglas 2FA provienen del ingles Two Factor Authentication (autenticación de dos factores) y TOTP de las siglas Time-based One-Time Password (Contraseña de un solo uso basada en el tiempo).

Como funciona

Cuando accedemos a nuestra cuenta de usuario lo hacemos mediante nuestro usuario y una contraseña, pero ahora vamos añadir un sistema de autenticación con el cual realizaremos una segunda validación.

Con este sistema ya tendríamos la autenticación de dos factores (2FA), pero necesitamos dotar a este sistema de mayor seguridad y por eso en los últimos años se ha añadido una nueva característica, un algoritmo conocido como contraseñas de un solo uso basadas en tiempo (TOTP).

Esta técnica nos ofrece mayor seguridad ya que nos permite validar nuestra identidad mediante un código generado en paralelo en un servidor y en una aplicación (normalmente en el teléfono), que se genera y actualiza cada 30 segundos.

Todo este conjunto nos ofrece un sistema muy seguro, ya que necesitamos conocer el usuario, la contraseña y además tener registrado en nuestro teléfono un sistema de validación que esta constantemente generando códigos de un solo uso y con un tiempo limitado para su uso.

Tecnologías y funcionamiento

La aplicación la he creado usando Javascript en el Front End, Node.js en el Back End y para el estilo visual he usado la librería de componentes Metro UI.

qr-app-2fa-totpComo podemos ver en la imagen la aplicación es extremadamente sencilla, simplemente pulsamos en el botón para generar un nuevo código QR.

Con este código registraremos el token de autorización y validación en Google Authenticator, el cual nos generará automáticamente cada 30 segundos un nuevo código de autorización.

google-authenticatorSolo queda añadir el código numérico que ha generador Google Authenticator y comprobarlo en la aplicación.

Como hemos podido ver, es un sistema muy sencillo de integrar en cualquier aplicación y nos permite asegurar el acceso a nuestras aplicaciones.

Para finalizar, desde aquí podéis acceder al repositorio para obtener el código fuente completo.

chat realtime cover

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Con esta nueva entrada quiero mostrar como he creado un chat usando NodeJS Socket.io. Para realizar el chat he usado la guía que ofrece la librería socket.io y su extensa documentación.

El código del proyecto

Para realizar este pequeño proyecto no he usado ningún framework basado en Javascript, ya que en principio iba a ser una prueba de concepto con pocas líneas para entender como funciona la librería socket.io.

Revisando la documentación oficial de socket.io se puede desarrollar un chat básico sin problema, pero como la idea era investigar, he probado algunas cosas añadiendo las siguientes funcionalidades:

  • Subida de ficheros
  • Chat privados
  • Información visual sobre los nuevos mensajes.
  • Cerrar y tener salas privadas

A continuación veremos el código correspondiente a la vista que he realizado usando bulma, el cual aconsejo echar un ojo ya que además de ligero, es bastante completo tanto en la documentación como en la cantidad de ejemplos de código.

index.html

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CHAT</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" />
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="notification hidden">
    <p></p>
  </div>
  <div class="container">
    <div class="columns">
      <div class="column is-one-quarter mt-6">
        <div class="card  has-background-black">
          <div class="card-content">
            <div class="media mb-0">
              <div class="media-content">
                <p class="title is-6 has-text-white">Usuarios conectados</p>
                <hr>
              </div>
            </div>
            <div class="content has-text-white" id="userConnected">
            </div>
          </div>
        </div>
      </div>
      <div class="column">
        <div class="tabs is-boxed pt-6 mb-0">
          <ul>
            <li id="tabGeneral" data-content="chatGeneral">
              <a class="active">
                <span class="has-text-white">General</span>
                <i class="fas fa-comment newMessage hide"></i>
              </a>
            </li>
          </ul>
        </div>
        <div class="columns">
          <div class="column containerChats pb-0">
            <div id="chatGeneral" class="chat textarea contentTab"></div>
            <div class="infoInput"></div>
          </div>
        </div>
        <div class="columns is-mobile">
          <div class="column is-one-quarter pr-0 pt-1">
            <input class="input has-text-white has-background-black" type="text" id="user" placeholder="Usuario">
          </div>
          <div class="column ml-1 mt-1 mr-3">
            <div class="columns is-mobile">
              <input class="input has-text-white has-background-black" type="text" id="message" placeholder="Mensaje" title="Subir fichero">
              <form>
                <button class="button is-info" id="btnUploadFile">
                  <i class="fas fa-file-upload"></i>
                  <input class="file-input" type="file" name="uploadFile" accept="image/x-png,image/gif,image/jpeg">
                </button>
              </form>
            </div>
          </div>
        </div>
        <div class="columns is-mobile hidden" id="containerProgress">
          <div class="column">
            <progress id="upload-progress-bar" max="100" value="0">0</progress>
            <div class="info-progress" />
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="/socket.io/socket.io.js"></script>
  <script src="./index.js"></script>
</body>
</html>

style.css

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300&display=swap');
html {
  overflow: hidden;
}
body {
  font-family: 'Montserrat', sans-serif;
  height: 100vh;
  width: 100vw;
  margin: 0;
  padding: 0;
  background: #354b52;
  color: #ffffff;
  overflow: hidden;
}
input {
  padding: 10px;
  font-size: 14px;
}
.chat {
  width: 100%;
  height: 250px;
  padding: 10px;
  font-size: 14px;
  overflow-y: auto;
  border: 1px solid #dddddd;
  background: #000000;
  color: #ffffff;
  resize: none !important;
}
.infoInput {
  font-size: 11px;
  position: relative;
  left: 10px;
  top: -5px;
}
.disabled {
  pointer-events: none;
  color: #aaaaaa !important;
}
.chatUser {
  font-weight: bold;
  margin-right: 5px;
}
::placeholder {
  color: #ffffff !important;
  opacity: .3 !important;
}
.chat::-webkit-scrollbar,
#userConnected::-webkit-scrollbar,
.tabs ul::-webkit-scrollbar {
  width: 10px;
}
.chat::-webkit-scrollbar-track,
#userConnected::-webkit-scrollbar-track,
.tabs ul::-webkit-scrollbar-track {
  border-radius: 10px;
}
.chat::-webkit-scrollbar-thumb,
#userConnected::-webkit-scrollbar-thumb,
.tabs ul::-webkit-scrollbar-thumb {
  background: #777777;       
  border-radius: 3px;
}
.chat::-webkit-scrollbar-thumb:hover,
#userConnected::-webkit-scrollbar-thumb:hover,
.tabs ul::-webkit-scrollbar-thumb:hover {
  background: #ffffff; 
  cursor: pointer;
}
.tabs ul {
  border-bottom: 0 !important;
  font-size: 14px;
  overflow: auto;
}
.tabs.is-boxed a:hover, .active {
  background-color: #000000;
  border: 1px solid #ffffff;
}
.containerChats .infoInput {
  text-align: right;
  left: 0;
  top: 0px;
  height: 15px;
}
.hide {
  display: none;
}
.card {
  border: 1px solid #dddddd;
  max-height: 343px;
  height: 343px;
}
.notification {
  transition: 300ms ease-in-out;
  position: fixed;
  bottom: -24px;
  width: 100%;
  opacity: 1;
  z-index: 10;
}
.notification.hidden {
  transition: 300ms ease-in-out;
  bottom: -100px;
  opacity: 0;
  z-index: -1;
}
#userConnected {
  overflow-y: auto;
  max-height: 225px;
  height: 225px;
}
#userConnected .chatUser {
  cursor: pointer !important;
}
@media (max-width: 768px) {
  .tabs  {
    padding-top: 0px !important;
  }
}
.newMessage {
  font-size: 11px;
  left: 5px;
  position: relative;
  color: #2196F3;
}
.closeChat {
  position: relative;
  left: 12px;
  top: -11px;
  color: #d46a10;
  font-size: 10px;
}
@media all and (max-width: 1023px) {
  .container {
    max-width: 960px;
    margin-left: 32px !important;
    margin-right: 32px !important;
  }
}
progress {
  width: 100%;
  position: relative;
  bottom: 25px;
  transition: 200ms ease-in-out;
  opacity: 1;
}
.info-progress {
  font-size: 11px;
  text-align: right;
  width: 100%;
  bottom: 30px;
  position: relative;
}
.hidden {
  opacity: 0;
  transition: 200ms ease-in-out;
}

Sobre el código anterior no hay mucho que añadir, una página simple pero suficiente para la prueba de concepto.

El último trozo de código que veremos, es el encargado de que la aplicación funcione en nuestro navegador.

index.js

// Definición de constantes y variables para la gestión del chat
let socket = null;
let user = null;
let message = null;
let usersPanel = null;
let notification = null;
let uploadFile = null;
let webrtc = null;
const CHAT_GENERAL = 'chatGeneral';
const LENGTH_MIN_USERNAME = 3;
const EMPTY = 0;
const LITERAL = {
  sameUser: 'No puedes enviarte un mensaje a ti mismo',
  minSizeUser: `El nombre del usuario debe tener <b>mínimo ${LENGTH_MIN_USERNAME} caracteres</b>`,
  uploadFile: 'Ha compartido un fichero',
  uploadSuccess: 'El fichero se ha subido y se esta compartiendo correctamente',
};

/** 
 * @description Cierra una sala de chat privada
 * @param {string} selectorID ID de la sala a cerrar
 */
const closeChat = selectorID => {
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).removeEventListener('click', selectedChat);
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).remove();
  document.querySelector(`.containerChats #${selectorID}`).remove();
  document.querySelector(`li[data-content='chatGeneral']`).click();
};

/** 
 * @description Creamos una sala privada
 * @param {object} data Información para crear la sala
 */
const chatTo = data => {
  const selectorID = data.idHTML || data.idOrigenHTML;
  const selectorIdUser = data.idUser ||data.idOrigen;
  const selectorUser = data.user || data.userOrigen;
  const existChat = document.querySelectorAll(`#${selectorID}`).length;
  if (existChat === EMPTY) {
    if (user.dataset.idhtml !== selectorID || data.idDestinoHTML !== data.idOrigenHTML) {
      document.querySelectorAll('.tabs ul li').forEach(item => {
        item.children[0].classList.remove('active');
      });
      document.querySelectorAll('.containerChats .chat').forEach(item => {
        item.classList.add('hide');
      });
      const chat = document.createElement('div');
      const li = document.createElement('li');
      const a = document.createElement('a');
      const span = document.createElement('span');
      const iNewChat = document.createElement('i');
      const iCloseChat = document.createElement('i');
      chat.setAttribute('id', selectorID);
      chat.classList.add('chat','textarea', 'contentTab');
      document.querySelector('.containerChats').prepend(chat);
      li.setAttribute('data-content', selectorID.toString());
      li.setAttribute('data-idUser', selectorIdUser.toString());
      a.classList.add('active');
      span.style.color = data.color;
      span.classList.add('has-text-white', 'chatUser');
      span.innerHTML = selectorUser;
      a.appendChild(span);
      iNewChat.classList.add('fas', 'fa-comment', 'newMessage', 'hide');
      a.appendChild(iNewChat);
      iCloseChat.classList.add('fas', 'fa-times-circle', 'closeChat');
      iCloseChat.onclick = () => { closeChat(selectorID.toString()); };
      a.appendChild(iCloseChat);
      li.appendChild(a);
      document.querySelector('.tabs ul').appendChild(li);
      document.querySelector(`.tabs ul li[data-content='${selectorID}']`).addEventListener('click', selectedChat);
    } else {
      notify(LITERAL.sameUser, 'danger');
    }
  }
};

/** 
 * @description Seleccionar una sala para charlar
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const selectedChat = evt => {
  document.querySelectorAll('.containerChats .chat').forEach(item => {
    item.classList.add('hide');
  });
  const showChat = evt.currentTarget.dataset['content'];
  const chatElement = document.querySelector(`#${showChat}`);
  if (chatElement) {
    chatElement.classList.remove('hide');
  }
  document.querySelectorAll('.tabs a').forEach(item => item.classList.remove('active'));
  evt.currentTarget.children[0].classList.add('active');
  if (!Array.from(evt.currentTarget.children[0].querySelector('i').classList).includes('hide')) {
    evt.currentTarget.children[0].querySelector('i').classList.add('hide')
  }
};

/** 
 * @description Envia mensajes al chat general y a un usuario privado
 * @param {object} evt Evento implicito en la acción ejecutada
 * @param {boolean} status Indicamos el estado para saber que usuario esta escribiendo
 */
const sendMessage = (evt, status) => {
  if (evt.key === 'Enter') {
    if (user.value.length >= LENGTH_MIN_USERNAME && message.value.trim().length > EMPTY) {
      const parent = document.querySelectorAll('.tabs a.active')[0].parentElement;
      const content = parent.dataset.content;
      const currentlyChat = document.querySelector(`#${content}`);
      const isChatGeneral = content === CHAT_GENERAL;
      const infoMensaje = {
        idOrigen: user.dataset.iduser,
        idOrigenHTML: user.dataset.idhtml,
        idDestino: isChatGeneral ? CHAT_GENERAL : parent.dataset.iduser,
        idDestinoHTML: content,
        userOrigen: user.value,
        userDestino: isChatGeneral ? CHAT_GENERAL : parent.innerText,
        message: message.value
      };
      if (content !== CHAT_GENERAL) {
        const color = document.querySelector(`#userConnected .chatUser[data-idhtml='${user.dataset.idhtml}']`).style.color;
        const messageSend = document.querySelector(`.containerChats #${content}`);
        const div = document.createElement('div');
        const span = document.createElement('span');
        span.style.color = color;
        span.classList.add('chatUser');
        span.appendChild(document.createTextNode(`${infoMensaje.userOrigen}:`));
        div.appendChild(span);
        div.innerHTML += infoMensaje.message;
        messageSend.appendChild(div);
      }
      socket.emit(`message-chat-${isChatGeneral ? 'general' : 'private'}`, infoMensaje);
      message.value = '';
      currentlyChat.scrollTo(0, currentlyChat.scrollHeight);
    } 
    user.classList[user.value.length >= LENGTH_MIN_USERNAME ? 'remove' : 'add']('has-background-danger');
  } else {
    if (user.value.length >= LENGTH_MIN_USERNAME) {
      if (evt.key !== 'Tab') {
        socket.emit('write-client', { data: status ? user.value : ''});
      }
    }
  }
};

/** 
 * @description Muestra texto indicando quien esta escribiendo
 * @param {object} payload Información que pinta cuando pulsamos un tecla
 */
const clientBeenWriting = payload => {
  document.querySelector('.containerChats .infoInput').innerHTML = payload;
};

/** 
 * @description Muestra texto en el chat general
 * @param {object} data Información relativa al usuario
 */
const reciveMessage = data => {
  const {
    color,
    idDestinoHTML,
    idOrigenHTML,
    userOrigen,
    message 
  } = data;
  let activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  if (!activeChatID) {
    chatTo(data);
    activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  }
  if (idDestinoHTML !== idOrigenHTML) {
    const div = document.createElement('div');
    const span = document.createElement('span');
    span.style.color = color;
    span.classList.add('chatUser');
    span.appendChild(document.createTextNode(`${userOrigen}:`));
    div.appendChild(span);
    div.innerHTML += message;
    activeChatID.appendChild(div);
    activeChatID.scrollTo(0, activeChatID.scrollHeight);
  }
  const newMessage = document.querySelector('.tabs a.active').parentElement.dataset.content
  if (newMessage !== activeChatID.id && newMessage !== idDestinoHTML) {
    const destino = document.querySelector(`.tabs li[data-content='${idDestinoHTML}'] i`);
    const origen = document.querySelector(`.tabs li[data-content='${idOrigenHTML}'] i`);
    (destino || origen).classList.remove('hide');
  }
};

/** 
 * @description Añade y actualiza el panel lateral con el listado de los usuarios
 * @param {object} payload Información de cada uno de los usuarios
 */
const registerUser = payload => {
  Array.from(usersPanel.children).forEach(item => item.remove())
  payload.forEach(item => {
    const div = document.createElement('div');
    div.style.color = item.color;
    div.classList.add('chatUser');
    div.onclick = () => { chatTo(item); };
    div.setAttribute('data-idUser', item.idUser);
    div.setAttribute('data-idHTML', item.idHTML);
    div.appendChild(document.createTextNode(item.user));
    usersPanel.appendChild(div);
    if (user.value === item.user) {
      user.setAttribute('data-idHTML', item.idHTML);
      user.setAttribute('data-idUser', item.idUser);
    }
  });
};

/** 
 * @description Muestra mensaje de error al registrar el usuario
 * @param {object} payload Información de cada uno de los usuarios
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const errorRegisteredUser = (payload, evt) => {
  const { error } = payload;
  notify(error, 'danger');
  evt.target.classList.remove('disabled');
  user.classList.add('has-background-danger');
};

/** 
 * @description Realiza la conexion al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const connectedToServer = evt => {
  const target = evt.target;
  if (target.value.length >= LENGTH_MIN_USERNAME) {
    target.classList.add('disabled');
    user.classList.remove('has-background-danger');
    socket = io.connect();
    socket.on('recive-message', reciveMessage);
    socket.on('registered-user', registerUser);
    socket.on('error-registered-user', payload => errorRegisteredUser(payload, evt));
    socket.on('client-been-writing', clientBeenWriting);
    socket.on('upload-progress', data => uploadProgress(data));
    const idHTML = generateIDHtml();
    socket.emit('connected-to-server', { user: target.value, idHTML });
  } else {
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Mostramos el progreso de la subida
 * @param {object} data Información sobre la subida del archivo
 */
const uploadProgress = data => {
  const { recived, total, who } = data;
  const porcent = Math.floor((recived * 100) / total);
  const currentlySize = (recived / 1024) / 1024; // MB
  const totalSize = (total / 1024) / 1024; // MB
  const progress = document.querySelector('#upload-progress-bar');
  const infoProgress = document.querySelector('.info-progress');
  progress.value = Math.floor(porcent);
  progress.innerHTML = porcent.toFixed(2);
  infoProgress.innerHTML = `${currentlySize.toFixed(2)} MB ${totalSize.toFixed(2)} MB ${porcent} %`;
};

/** 
 * @description Cargamos un fichero y se sube al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const upload = async evt => {
  if (user.value.length >= LENGTH_MIN_USERNAME) {
    const uploadProgress = document.querySelector('#containerProgress');
    uploadProgress.classList.remove('hidden');
    const btnUploadFile = document.querySelector('#btnUploadFile');
    btnUploadFile.classList.add('is-loading');
    btnUploadFile.setAttribute('disabled', true);
    const files = evt.target.files;
    const data = new FormData();
    data.append('archivo', files[0]);
    socket.emit('upload-file', { idUser: user.dataset.iduser });
    const result = await (await fetch('/upload-file', {
      method: 'POST',
      body: data
    })).json();
    btnUploadFile.classList.remove('is-loading');
    btnUploadFile.removeAttribute('disabled');
    const { statusCode, path, statusMessage } = result;
    if (statusCode === 200) {
      message.value = `${LITERAL.uploadFile} <a href='${statusMessage}' target='_blank'>[${files[0].name}]</a>`;
      sendMessage(evt = {key: 'Enter'}, true);
      notify(LITERAL.uploadSuccess, 'success', 4000);
    } else {
      notify(statusMessage, 'danger', 4000);
    }
    setTimeout(() => {
      uploadProgress.classList.add('hidden');
    }, 2000);
  } else {
    document.querySelector('form').reset();
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Inicialización del chat
 */
const load = () => {
  const tabGeneral = document.querySelector('#tabGeneral');
  user = document.querySelector('#user');
  message = document.querySelector('#message');
  usersPanel = document.querySelector('#userConnected');
  notification = document.querySelector('.notification');
  message.addEventListener('keydown', evt => sendMessage(evt, true));
  message.addEventListener('keyup',  evt => sendMessage(evt, false));
  user.addEventListener('blur', connectedToServer);
  tabGeneral.addEventListener('click', selectedChat);
  uploadFile = document.querySelector('input[type=file]');
  uploadFile.addEventListener('change', upload);
};

/** 
 * @description Muestra notificaciones en el chat
 * @param {string} msn Mensaje que se muestra en la notificación
 * @param {string} type Tipo de mensaje (danger, info, success, warning)
 * @param {number} timeout Duración de la notificación
 */
const notify = (msn = '', type = 'info', timeout = 2000) => {
  notification.children[0].innerHTML = msn;
  notification.classList.add(`is-${type}`);
  notification.classList.remove('hidden');
  setTimeout(() => {
    notification.classList.add('hidden');
    notification.classList.remove(`is-${type}`);
  }, timeout);
};

/** 
 * @description Generamos un ID para cada usuario
 */
const generateIDHtml = () => new Date().getTime().toString().split('').map(i => String.fromCharCode(parseInt(i) + 65)).join('');

/** 
 * @description Espera a que este la página completamente cargada
 */
document.addEventListener("DOMContentLoaded", load);

En un futuro me gustaría realizar esta misma aplicación usando React Vue, entre otras cosas para aprovechar la magia que ofrecen y poder añadir mas características como, llamadas de audio y video, compartir pantalla, guardar conversaciones, stickers, gifs, etc…

Para finalizar esta entrada, si estás interesado en usar el código, lo puedes obtener desde Github y ademas puedes probar el chat en la siguiente url: https://chat.tecnops.es/

youtube mp3 cover

Descargar MP3 de Youtube

En esta ocasión y a falta de terminar la segunda entrega del tutorial / guía de referencia sobre Javascript y sus mejoras, he decido crear una pequeña aplicación que hacia tiempo tenia ganas de crear.

La aplicación es para convertir y descargar el audio MP3 de cualquier vídeo de Youtube.

Para llevar a cabo esta aplicación he usado las siguiente herramientas:

  • Para el diseño de la aplicación he usado Materialize CSS que es bastante simple y tiene un buen acabado.
  • La lógica esta realizada en Javascript nativo ya que tampoco tiene mucha complicación.
  • Para la descarga / conversión he usado NodeJS y algunas dependencias extras como ffmpeg, express y ytdl-core

A continuación un pequeño vídeo de la aplicación en funcionamiento y pulsando aquí puedes usarla.