Recuperar los títulos de las canciones de un CD con FREEDB

Informática 3 Comentarios

Desde hace mucho tiempo estan disponibles dos servicios en Internet cuyo cometido es almacenar una gigantesca base de datos con los títulos de las canciones de cualquier CD de música que se haya editado. La primera es Gracenote (también conocida antes del 2000 por cddb), de la cual no hablaremos en este artículo por requerir licencia. La segunda es freedb la cual sigue siendo gratuita y con una simple página web podemos consultar la información que nos interesa. En la sección developers podreis encontrar la información necesaria para trabajar con esta tecnología.

Básicamente consiste en obtener una identificación única a partir de la información de la tabla de contenidos de un CD (TOC). Una vez obtenida la identificación, solo hay que hacer una petición web al servidor de freedb con la misma para que nos devuelva el título del CD y la lista de títulos de canciones y sus respectivos autores de las distintas pistas que conforman el CD.

El algoritmo para hallar el identificador único consiste en:
-Sumar los números en segundos de lo que dura cada pista. Es decir, si una cancion dura 4:17, entónces son 257 segundos, luego la suma es 2+5+7=14.
-Una vez obtenido el sumatorio de todas las pistas con el anterior algoritmo, se le saca el módulo de 255 y a continuación se le multiplica por 16777216 (2^24); a este resultado se le suma el tiempo total del CD multiplicado por 256 (2^8) y finalmente a este resultado se le suma el número total de pistas que contiene el CD.

Una vez hallado el identificador único, se transforma a un número hexadecimal y se hace una peticion a freedb de la siguiente manera:

http://freedb.freedb.org/~cddb/cddb.cgi?cmd=cddb+read+misc+<ID UNICO>&hello=name+host.com+appname+1.0&proto=1

cddb read misc <ID UNICO> del parámetro cmd es el comando que indica que estamos haciendo una petición a la base de datos con el ID UNICO. Por ejemplo cddb read misc c510910d.

name host.com appname 1.0 del parámetro hello indica que persona, dominio, aplicación y versión de la misma esta solicitando la información. Por ejemplo: sistemasorp sistemasorp.blogspot.com testeoCDDB 1.0.

Y finalmente el 1 del parámetro proto indica que versión del protocolo queremos usar para hacer la petición a la base de datos, devolviendonos acorde a la versión una información formateada.

Así por ejemplo si lanzamos esta petición:

http://freedb.freedb.org/~cddb/cddb.cgi?cmd=cddb+read+misc+c510910d&hello=sistemasorp+sistemasorp.blogspot.com+testeoCDDB+1.0&proto=1

Nos devuelve lo siguiente (terminado en una linea con un punto, al estilo del SMTP):

210 misc c510910d CD database entry follows (until terminating `.’)
# xmcd CD database file
#
# Track frame offsets:
# 150
# 24460
# 45650
# 71127
# 98425
# 118997
# 146585
# 164707
# 188897
# 216340
# 240932
# 271642
# 297722
#
# Disc length: 4243 seconds
#
# Revision: 2
# Processed by: cddbd v1.5PL3 Copyright (c) Steve Scherf et al.
# Submitted via: CDex 1.40Beta9
#
DISCID=c510910d
DTITLE=Various / Rave Massacre [Disk 1]
TTITLE0=Raving Bastards – Love Time
TTITLE1=Chill’n Force – Move Raver (Kemo Mix)
TTITLE2=M.A.F. X-Perience – Dreamland
TTITLE3=Society for Psychical Research – Silversky
TTITLE4=Obsessiv – Tune In, Tune On, Drop Out
TTITLE5=Nettuno – I Cry
TTITLE6=Razor – Is It Love
TTITLE7=Sunbeam – Outside World
TTITLE8=Raver’s Nature – Tricky Symphony
TTITLE9=NIP Collective – I’m About
TTITLE10=RBM – Banyo Love
TTITLE11=Paranoia X – Party Program
TTITLE12=NR-Gizer – Raving Generation
EXTD= YEAR: 1994 ID3G: 31
EXTT0=
EXTT1=
EXTT2=
EXTT3=
EXTT4=
EXTT5=
EXTT6=
EXTT7=
EXTT8=
EXTT9=
EXTT10=
EXTT11=
EXTT12=
PLAYORDER=
.

Donde entre comentarios (con la almohadilla delante) da información sobre el disco, copyright y demás; continuando con una lista de parejas nombre/valor que contienen la información que queremos:

DISCID: El mismo identificador de disco que le hemos pasado
DTITLE: El autor y título del CD
TTITLEX: El título de canción que está en la pista X
EXTD: Información extendida sobre el disco
EXTTX: Información extendida sobre la canción que está en la pista X
PLAYORDER: El orden en el que se deberían reproducir las pistas
YEAR: El año de edición del disco
ID3G: La categoría o clase de música que contiene el CD

A continuación expongo dos codigos fuentes (el primero para windows y el segundo para linux) que muestran como recoger el identificador único de un cd y a continuación solicitar la información de ese cd a freedb. Ambos se ejecutan en la consola de texto pasandoles como parámetro la unidad o dispositivo de cd.

WINDOWS(ejecutable y fuentes)

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock.h>
#include <windows.h>
 
#define IOCTL_CDROM_READ_TOC 0x00024000
 
typedef struct {
  UCHAR  Reserved;
  UCHAR  Control : 4;
  UCHAR  Adr : 4;
  UCHAR  TrackNumber;
  UCHAR  Reserved1;
  UCHAR  Address[4];
} TRACK_DATA;
 
typedef struct {
  UCHAR  Length[2];
  UCHAR  FirstTrack;
  UCHAR  LastTrack;
  TRACK_DATA  TrackData[100];
} CDROM_TOC;
 
/*
Funcion:
 lee_CDTOC
Parámetros:
 sDispositivo: ruta del dispositivo de CD.
 toc: estructura donde se almancena la tabla de contenidos del CD.
Descripción:
 Esta función se encarga de guardar en la variable toc la información de la tabla de contenidos del CD ubicado en el dispostivo lector de cdrom.
*/
void lee_CDTOC(char *sLetra,CDROM_TOC *toc)
{
 HANDLE manejador;
 DWORD nTamBuffer;
 char sUnidad[7];
 
 sprintf(sUnidad,"\\\\.\\%s",sLetra);
 
 manejador = CreateFile(sUnidad, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
 if(manejador == INVALID_HANDLE_VALUE)
 {
  fprintf(stderr,"No se pudo acceder al disco de la unidad de CD\n");
  return;
 }
 
 if (DeviceIoControl(manejador, IOCTL_CDROM_READ_TOC, NULL, 0, toc, sizeof(CDROM_TOC), &nTamBuffer, NULL) == FALSE)
  fprintf(stderr,"No se pudo leer el contenido del CD\n");
 
 CloseHandle(manejador);
}
 
/*
Funcion:
 lee_InfoCDDB: Devuelve una cadena con los resultados de la petición.
Parámetros:
 sIDDisco: identificador único del cd.
 sBuffer: buffer donde almacenar los resultados de la petición.
Descripción:
 Esta función se encarga de pedir a freedb la información relacionada con un identificador de cd determinado y devolver todo el contenido de la petición.
*/
char *lee_InfoCDDB(char *sIDDisco,char *sBuffer)
{
 WSAData data;
 SOCKET sock;
 struct hostent *servidor;
 struct sockaddr_in destino;
 char sPeticion[256];
 int nEnviados,nRecibidos;
 
 if(WSAStartup(MAKEWORD( 2, 2 ),&data) == 0)
 {
  if((sock=socket(PF_INET,SOCK_STREAM,0)) == INVALID_SOCKET)
   fprintf(stderr,"No se pudo crear la conexion");
  else
  {
   if((servidor=gethostbyname("freedb.freedb.org")) == NULL)
    fprintf(stderr,"No se pudo obtener la IP del servidor");
   else
   {
    destino.sin_family = AF_INET;
    destino.sin_port = htons(80);
    destino.sin_addr = *((struct in_addr *)servidor->h_addr_list[0]);
    memset(&(destino.sin_zero), '', 8);
    if(connect(sock, (struct sockaddr *)&destino, sizeof(struct sockaddr)) == SOCKET_ERROR)
     fprintf(stderr,"No se pudo conectar al servidor");
    else
    {
     sprintf(sPeticion,"GET /~cddb/cddb.cgi?cmd=cddb+read+misc+%s&hello=sistemasorp+sistemasorp.blogspot.com+testeoCDDB+1.0&proto=1 HTTP/1.0\r\n",sIDDisco);
     strcat(sPeticion,"Host: freedb.freedb.org\r\n\r\n");
     nEnviados=send(sock,sPeticion,strlen(sPeticion),0);
     if(nEnviados == SOCKET_ERROR)
      fprintf(stderr,"No se pudo enviar la peticion al servidor");
     nRecibidos=0;
     while(strstr(sBuffer,"\r\n.\r\n") == NULL)
     {
      nRecibidos+=recv(sock,sBuffer+nRecibidos,1024,0);
      sBuffer[nRecibidos]='';
     }
 
    }
   }
  }
  WSACleanup();
 }
 else
  fprintf(stderr,"No se puedo inicializar la conexion");
 return sBuffer;
}
 
/*
Funcion:
 cddb_Suma: Devuelve un número sacado a través de una suma de cifras.
Parámetros:
 nDuracion: Duración en segundos de una pista.
Descripción:
 Esta función se encarga de sumar todas las cifras de distinto peso entre si para posteriormente devolver el resultado.
*/
int cddb_Suma(int nDuracion)
{
 int nSuma = 0;
 
 while (nDuracion > 0) {
  nSuma = nSuma + (nDuracion % 10);
  nDuracion = nDuracion / 10;
 }
 
 return nSuma;
}
 
/*
Funcion:
 cddb_IDDisco: Devuelve en número que identifica al CD inequivocamente.
Parámetros:
 toc: Información de la tabla de contenidos del CD
Descripción:
 Esta función se encarga de calcular el identificador único de un CD a través de su tabla de contenidos
*/
unsigned long cddb_IDDisco(CDROM_TOC *toc)
{
 int nIndice=0,
  nTotalTiempo = 0,
  nSumatorio = 0;
 int nTotalPistas;
 
 nTotalPistas=toc->LastTrack-toc->FirstTrack+1;
 
 while (nIndice < nTotalPistas) {
  nSumatorio = nSumatorio + cddb_Suma((toc->TrackData[nIndice].Address[1] * 60) + toc->TrackData[nIndice].Address[2]);
  nIndice++;
 }
 
 nTotalTiempo = ((toc->TrackData[toc->LastTrack].Address[1] * 60) + toc->TrackData[toc->LastTrack].Address[2]) -
     ((toc->TrackData[0].Address[1] * 60) + toc->TrackData[0].Address[2]);
 
 return ((nSumatorio % 0xff) << 24 | nTotalTiempo << 8 | nTotalPistas);
}
 
int main(int argc, char *argv[ ])
{
 CDROM_TOC toc;
 char sIDDisco[9];
 char *sBuffer;
 
 if(argc==2)
 {
  memset(&toc,0,sizeof(CDROM_TOC));
 
  lee_CDTOC(argv[1],&toc);
  if(toc.LastTrack>0)
  {
   sprintf(sIDDisco,"%08x",cddb_IDDisco(&toc));
   printf("El ID del disco es %s\n",sIDDisco);
 
   sBuffer=(char *)malloc(4096);
   if(sBuffer == NULL)
   {
    fprintf(stderr,"No se pudo alojar memoria");
    return 1;
   }   
   *sBuffer='';
   lee_InfoCDDB(sIDDisco,sBuffer);
   if(*sBuffer=='')
    return 1;
   printf("La información del disco es:\n%s",sBuffer);
   free(sBuffer);
  }
  else
   return 1;
 }
 else
 {
  fprintf(stderr,"La sintaxis es: %s <unidad de cd>\nEjemplo: %s d:\n",argv[0],argv[0]);
  return 1;
 }
 
 return 0;
}

LINUX(ejecutable y fuentes)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdarg.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
 
#if defined linux
#include <linux/cdrom.h>
#elif defined sun
#include <sys/cdio.h>
#endif
 
typedef struct {
 int minutos, segundos;
} TRACK_DATA;
 
typedef struct {
  unsigned char  FirstTrack;
  unsigned char LastTrack; 
  TRACK_DATA  TrackData[100];
} CDROM_TOC;
 
/*
Funcion:
 lee_CDTOC
Parámetros:
 sDispositivo: ruta del dispositivo de CD.
 toc: estructura donde se almancena la tabla de contenidos del CD.
Descripción:
 Esta función se encarga de guardar en la variable toc la información de la tabla de contenidos del CD ubicado en el dispostivo lector de cdrom.
*/
void lee_CDTOC(char *sDispositivo,CDROM_TOC *toc)
{
 struct cdrom_tochdr tochdr;
 struct cdrom_tocentry tocentry;
 int nIndice, manejador;
 
 manejador= open(sDispositivo, O_RDONLY | O_NONBLOCK);
 if(manejador == -1)
 {
  fprintf(stderr,"No se pudo acceder al disco de la unidad de CD\n");
  return;
 }
 
 if (ioctl(manejador, CDROMREADTOCHDR, &tochdr) == -1)
  fprintf(stderr,"No se pudo leer el contenido del CD\n");
 else
 {
  toc->FirstTrack=tochdr.cdth_trk0;
  toc->LastTrack=tochdr.cdth_trk1;
  for (nIndice = toc->FirstTrack; nIndice <= toc->LastTrack; nIndice++) 
  {
   tocentry.cdte_track = nIndice;
   tocentry.cdte_format = CDROM_MSF;
   ioctl(manejador, CDROMREADTOCENTRY, &tocentry);
   toc->TrackData[nIndice-1].minutos = tocentry.cdte_addr.msf.minute;
   toc->TrackData[nIndice-1].segundos = tocentry.cdte_addr.msf.second;
  }
  tocentry.cdte_track = 0xAA;
  tocentry.cdte_format = CDROM_MSF;
  ioctl(manejador, CDROMREADTOCENTRY, &tocentry);
  toc->TrackData[toc->LastTrack].minutos = tocentry.cdte_addr.msf.minute;
  toc->TrackData[toc->LastTrack].segundos = tocentry.cdte_addr.msf.second;
 }
 close(manejador);
}
 
/*
Funcion:
 lee_InfoCDDB: Devuelve una cadena con los resultados de la petición.
Parámetros:
 sIDDisco: identificador único del cd.
 sBuffer: buffer donde almacenar los resultados de la petición.
Descripción:
 Esta función se encarga de pedir a freedb la información relacionada con un identificador de cd determinado y devolver todo el contenido de la petición.
*/
char *lee_InfoCDDB(char *sIDDisco,char *sBuffer)
{
 int sock;
 struct hostent *servidor;
 struct sockaddr_in destino;
 char sPeticion[256];
 int nEnviados,nRecibidos;
 
 if((sock=socket(PF_INET,SOCK_STREAM,0)) == -1)
  fprintf(stderr,"No se pudo crear la conexion");
 else
 {
  if((servidor=gethostbyname("freedb.freedb.org"))==NULL)
   fprintf(stderr,"No se pudo obtener la IP del servidor");
  else
  {
   destino.sin_family = AF_INET;
   destino.sin_port = htons(80);
   destino.sin_addr = *((struct in_addr *)servidor->h_addr_list[0]);
   memset(&(destino.sin_zero), '', 8);
   if(connect(sock, (struct sockaddr *)&destino, sizeof(struct sockaddr)) == -1)
    fprintf(stderr,"No se pudo conectar al servidor");
   else
   {
    sprintf(sPeticion,"GET /~cddb/cddb.cgi?cmd=cddb+read+misc+%s&hello=sistemasorp+sistemasorp.blogspot.com+testeoCDDB+1.0&proto=1 HTTP/1.0\r\n",sIDDisco);
    strcat(sPeticion,"Host: freedb.freedb.org\r\n\r\n");
    nEnviados=send(sock,sPeticion,strlen(sPeticion),0);
    if(nEnviados == -1)
     fprintf(stderr,"No se pudo enviar la peticion al servidor");
    nRecibidos=0;
    while(strstr(sBuffer,"\r\n.\r\n") == NULL)
    {
     nRecibidos+=recv(sock,sBuffer+nRecibidos,1024,0);
     sBuffer[nRecibidos]='';
    }
 
   }
  }
 }
 return sBuffer;
}
 
/*
Funcion:
 cddb_Suma: Devuelve un número sacado a través de una suma de cifras.
Parámetros:
 nDuracion: Duración en segundos de una pista.
Descripción:
 Esta función se encarga de sumar todas las cifras de distinto peso entre si para posteriormente devolver el resultado.
*/
int cddb_Suma(int nDuracion)
{
 int nSuma = 0;
 
 while (nDuracion > 0) {
  nSuma = nSuma + (nDuracion % 10);
  nDuracion = nDuracion / 10;
 }
 
 return nSuma;
}
 
/*
Funcion:
 cddb_IDDisco: Devuelve en número que identifica al CD inequivocamente.
Parámetros:
 toc: Información de la tabla de contenidos del CD
Descripción:
 Esta función se encarga de calcular el identificador único de un CD a través de su tabla de contenidos
*/
unsigned long cddb_IDDisco(CDROM_TOC *toc)
{
 int nIndice=0,
  nTotalTiempo = 0,
  nSumatorio = 0;
 int nTotalPistas;
 
 nTotalPistas=toc->LastTrack-toc->FirstTrack+1;
 
 while (nIndice < nTotalPistas) {
  nSumatorio = nSumatorio + cddb_Suma((toc->TrackData[nIndice].minutos * 60) + toc->TrackData[nIndice].segundos);
  nIndice++;
 }
 
 nTotalTiempo = ((toc->TrackData[toc->LastTrack].minutos* 60) + toc->TrackData[toc->LastTrack].segundos) -
     ((toc->TrackData[0].minutos * 60) + toc->TrackData[0].segundos);
 
 return ((nSumatorio % 0xff) << 24 | nTotalTiempo << 8 | nTotalPistas);
}
 
int main(int argc, char *argv[ ])
{
 CDROM_TOC toc;
 char sIDDisco[9];
 char *sBuffer;
 
 if(argc==2)
 {
  memset(&toc,0,sizeof(CDROM_TOC));
 
  lee_CDTOC(argv[1],&toc);
  if(toc.LastTrack>0)
  {
   sprintf(sIDDisco,"%08x",cddb_IDDisco(&toc));
   printf("El ID del disco es %s\n",sIDDisco);
 
   sBuffer=(char *)malloc(4096);
   if(sBuffer == NULL)
   {
    fprintf(stderr,"No se pudo alojar memoria");
    return 1;
   }
   *sBuffer='';
   lee_InfoCDDB(sIDDisco,sBuffer);
   if(*sBuffer=='')
    return 1;
   printf("La información del disco es:\n%s",sBuffer);
   free(sBuffer);
  }
  else
   return 1;
 }
 else
 {
  fprintf(stderr,"La sintaxis es: %s <dispositivo de cd>\nEjemplo: %s /dev/cdrom\n",argv[0],argv[0]);
  return 1;
 }
 
 return 0;
}

webchat en tiempo real con php y mysql

Uncategorized 18 Comentarios

Después de un pequeño descanso volvemos a las andadas. Esta vez voy a mostrar como crear un webchat en tiempo real.

El problema de los webchat es que la inmensa mayoría de los que existen se dedican a refrescar la página de la charla cada X tiempo o cuando el usuario ha escrito un mensaje. Esto es debido a que el protocolo HTTP esta diseñado para que una vez recibido los datos cierre la conexión, y si queremos recuperar datos nuevos tenemos que hacer una nueva petición.

Para lograr una interactividad y un realismo temporal cercanos al irc, messenger, etc. y seguir usando la web tenemos que usar un truco, y es crear una página que nunca termine de ejecutarse. Quizá os pregunteis que si nunca termina de ejecutarse, entónces no saldrá ningún resultado, pues bién, podemos desactivar el buffer del php desde el principio o hacer un volcado continuo para que cada dato nuevo se vaya enviando al navegador del cliente. Los navegadores muestran el contenido según les va llegando, por lo que una sentencia como esta (en pseudocodigo):

mientras verdadero
recupera mensajes nuevos
muestra mensajes nuevos
fin del mientras

haría que cada vez que un mensaje nuevo apareciese se enviase al navegador y este lo mostrara instantaneamente, con lo que habríamos conseguido que fuese el tiempo real.

Parar lograr este planteamiento en php tenemos que seguir los siguientes pasos (he reducido su código para simplificarlo):

ob_start();

Que activa el tratamiento del buffer en php

set_time_limit(0);

Indica al motor de php que el script nunca debe caducar a pesar de lo que dure.


for(;;)
{

Este es el bucle que constantemente se va repitiendo.


$rs=mysql_query(“SELECT * FROM chat_mensajes WHERE IDMensaje>’” . $id . “‘ AND WEEK(DatFecha,1)=WEEK(NOW(),1) AND YEAR(DatFecha)=YEAR(NOW()) ORDER BY IDMensaje”)
or die (“No se puede realizar la consulta”);

En mi caso guardo los mensajes en una base de datos mysql, por lo que con la consulta SQL compruebo aquellos mensajes cuyo ID sea mayor que el último ID de mensaje que se ha mostrado (solo aquellos dentro de la semana actual del año en curso).


if(mysql_num_rows($rs)>0)
{
while($fila=mysql_fetch_array($rs,MYSQL_ASSOC)) {

Compruebo si hay nuevos mensajes y los recorro.


$id=$fila["IDMensaje"];

Voy actualizando la variable id para que contenga al final el último ID y así en la siguiente pasada del bucle solo recoger los últimos mensajes escritos a partir del indicado.


echo “<div>\n”;
echo “<span>” . $fila["DatFecha"] . ” ” . “</span>”;
echo “<span>” . $fila["StrUsuario"] . “&gt;</span>”;
echo “<span>” . $fila["StrMensaje"] . “</span>”;
echo “</div>\n”;
}

Se va mostrando el contenido de los mensajes (fecha, remitente y mensaje).


echo “<script>scrollTo(0,999999999);</script>\n”;
ob_flush();
flush();
}

Con un pequeño truco en javascript logramos desplazar la ventana del navegador siempre hasta el final del contenido. A continuacuón vaciamos el buffer hacia el navegador del cliente.


sleep(1);
}

Para no recargar mucho el servidor mysql haciendo constantemente consultas, pausamos durante un segundo la ejecución para después continuar con la siguiente iteración del bucle.

Quizá penseis que esta solución recarga mucho el servidor (tanto el web como el de mysql), pero el impacto es mínimo y si teneis un hosting esto os ahorrará ancho de banda puesto que gasta más por usuario el estar haciendo peticiones al servidor web cada X tiempo que una sola petición que dure eternamente (solo gasta lo que envia, como una petición normal); en cuanto al servidor mysql si este esta en localhost (como el 99% de los hosting) el impacto es mínimo porque no consume ningún recurso de red, además de que el propio mysql es rapídisimo y eficiente haciendo las consultas y no mermará el rendimiento por estar haciendo una cada segundo por cada usuario. Sin embargo si no quereis usar mysql, podeis usar ficheros (fopen) o bién memoria compartida (Shmop). Por supuesto esto no es la panacea, pero para cosas pequeñas (20 personas a la vez) si puede ir bién tenerlo así montado.

En la web en la que colaboro tengo implementado este sistema, que aunque lo tengo bastante desarollado, todavía tengo que mejorar ciertos aspectos de funcionalidad.