UNIVERSIDAD DE CONCEPCIÓN FACULTAD DE INGENIERÍA DEPARTAMENTO DE INGENIERÍA ELÉCTRICA Profesor Patrocinante: Dr. Mario R. Medina C. Informe de Memoria de Título para optar al título de: Ingeniero Civil Electrónico Desarrollo de una biblioteca para transmisión distribuida y colaborativa de música Concepción, Abril de 2012 Felipe Alberto Glaría Grego UNIVERSIDAD DE CONCEPCIÓN Facultad de Ingeniería Departamento de Ingeniería Eléctrica Profesor Patrocinante: Mario R. Medina C. Desarrollo de una biblioteca para transmisión distribuida y colaborativa de música Felipe Alberto Glaría Grego Informe de Memoria de Título para optar al título de Ingeniero Civil Electrónico Abril 2012 iii Resumen Ante la gran variedad de música a la que se puede acceder hoy en día, es necesario desarrollar sistemas que permitan filtrar de alguna manera todas las opciones disponibles, y así poder escuchar canciones y/o artistas conocidos o desconocidos que sean de nuestro agrado. En este informe se presenta la realización de una radio online con reproductor web, que permita evaluar las canciones reproducidas. Luego, mediante la técnica de filtrado colaborativo, se reconocen usuarios con gustos similares y se predicen nuevas evaluaciones para canciones no ingresadas. Usando el mismo sistema, se se identifican, además, los usuarios disímiles, y mediante la hipótesis “a las personas normalmente le gusta y disgustan ciertos estilos definidos”, se predicen evaluaciones inversas y, para luego, ponderando ambas predicciones, se obtiene una predicción final. Además, mediante distintos métodos de detección (HTML DOM, AJAX, jQuery), se identifican a los usuarios que escuchan la radio en línea, para así adaptar efectivamente el contenido a reproducir según los gustos de los oyentes en ese momento. Los resultados de este trabajo muestran que es posible identificar a los usuarios en ambos grupos y generar un listado de canciones adaptativo. Finalmente, se plantean variadas opciones para complementar la solución: mejorar la gestión de usuarios y canciones, usar filtrado colaborativo, optimizar el algoritmo, entre otras. Este sistema se implementa en un servidor disponible en el Departamento de Ingeniería Eléctrica de la Facultad de Ingeniería de la Universidad de Concepción. iv A mi tío Carlos... Por convencerme de volver v Agradecimientos Llegando al final de una larga etapa (más larga de lo pensado), sin duda que hay muchas personas a las cuales les debo más que un sencillo agradecimiento. Aún así, tengo el honor y gusto de agregarlos con mucho cariño a este trabajo. Imposible empezar esta sección por otra persona que no sea mi madre. Su amor, paciencia y apoyo fueron, son y serán por siempre constantes, invariables, indudables e incuestionables, y por ello estaré eternamente agradecido. Además, por si el lector no lo deduce aún, es la mujer que me trajo al mundo, y eso hace que la deuda y gratitud sea aún más enorme. Un gran beso escrito para ti, mamá. Y de embarazos espontáneos no se llega al mundo (debata el que quiera), el segundo en la lista es mi padre. Pese a la distancia, siempre se sintió el mismo apoyo antes mencionado, y aunque significó dejar de vivir juntos en algún momento, con este trabajo se cosecha el fruto de dicha decisión. Otro beso para ti, papá. La primera persona externa a mi familia, al menos de sangre, es a mi compañera, polola, pareja, Ale. Juntos casi el mismo tiempo que duró esta etapa (tampoco los 6 años respectivos), le agradezco el amor y paciencia que me entrega, ambos igual de enormes y significativos. Debes ser la persona con la cual compartí más de cerca lo bueno y malo de este viaje, y por eso muchas gracias, un nuevo beso para ti. A mi familia le agradezco más apoyo (imposible caerse así). Hermanos, tíos, tías, primos, primas, padrinos, madrinas, omi, y en especial a mi opa, mi tío Carlos, al cual nunca convencí del todo con mi aventura en la capital, y por ello he llegado a este momento ahora. Aunque no estás aquí para verlo, te inmortalizo en esta memoria, como te lo mereces. A mis amigos de toda la vida, de media vida y de este último cuarto también. Confío en que saben sin dudar que me refiero a ustedes, cómplices de innumerables anécdotas, pilares fundamentales más de alguna vez, y piezas esenciales en lo que depare el futuro. Son grandes. También le agradezco a todos los compañeros de universidad que no se sintieron incluidos en el párrafo anterior, ya que, si bien no fuimos grandes amigos en estos años, sí compartimos bastantes momentos gratos e ingratos, aprendimos muchas cosas útiles (y no tanto), sufrimos los mismos cursos y dormimos las mismas clases. Gracias por hacer más divertido este viaje. Agradezco haber tenido clases y trabajado con muchos profesores. Al Dr. Mario Medina, por enseñarme a programar con ejemplos simples y problemas a veces demasiado ocurrentes. Al Dr. Miguel Figueroa, por motivarme a aprender, y siempre estar disponible para conversar. A tantos profesores más, fue un honor haber tenido la oportunidad de aprender de ustedes. Al ánimo de muchos funcionarios de la universidad, en especial a Inés y su habilidad de controlar el espacio-tiempo para poder entregar tantos pre-informes e informes, y su paciencia y alegría siempre a disposición de resolver dudas académicas existenciales. Además, a todo aquel que cree haber tenido un efecto insignificante, pasajero e irrelevante en mi vida, pero mayor aún, a los que saben que tuvieron un efecto negativo en ella, ya que sin ustedes no sería la persona que soy actualmente, y ya sea para bien o para mal, me gusta el resultado obtenido. Finalmente, muchas gracias a ti, lector, por interesarte en estas palabras, y confío que de algo servirá que sigas avanzando... Tabla de contenidos vi Nomeclatura xii Abreviaciones xiii 1. Introducción 1.1. Introducción general . . . . 1.2. Objetivos . . . . . . . . . . 1.2.1. Objetivo general . . 1.2.2. Objetivos específicos 1.3. Alcances y limitaciones . . . 1.4. Temario . . . . . . . . . . . . . . . . . 2. Filtrado colaborativo 2.1. Introducción . . . . . . . . . . 2.2. Bases del filtrado colaborativo 2.3. Ventajas . . . . . . . . . . . . 2.4. Desventajas . . . . . . . . . . 2.5. Discusión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Hardware y software 3.1. Introducción . . . . . . . . . . . . 3.2. El servidor . . . . . . . . . . . . . 3.3. Software de streaming . . . . . . . 3.3.1. Icecast 2 . . . . . . . . . . 3.3.2. Ices . . . . . . . . . . . . 3.4. PHP y reproductor embebido web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4. Desarrollo de un algoritmo predictivo y listado adaptativo 4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . 4.2. Una radio online . . . . . . . . . . . . . . . . . . . . . 4.3. Generar la lista . . . . . . . . . . . . . . . . . . . . . 4.3.1. Adaptar evaluaciones . . . . . . . . . . . . . . 4.3.2. Predicción de evaluaciones . . . . . . . . . . . 4.3.3. Las más populares . . . . . . . . . . . . . . . 4.3.4. La próxima canción . . . . . . . . . . . . . . . 4.4. El algoritmo en Python . . . . . . . . . . . . . . . . . 4.4.1. Función create_matrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 1 1 2 2 2 . . . . . 3 3 3 4 4 5 . . . . . . 6 6 6 7 7 8 8 . . . . . . . . . 10 10 10 10 11 11 15 16 16 16 vii 4.4.2. 4.4.3. 4.4.4. 4.4.5. 4.4.6. 4.4.7. Función MDS . . . . . . Función CalculaPesos . . Función Predict . . . . . Función GetPredicciones Función TopTracks . . . Función PredictTop . . . . . . . . . . . . . . . . . . . . . 17 18 18 22 25 26 . . . . . . . . . . . . . . . . . . . 28 28 28 29 29 29 31 31 31 33 42 42 43 43 43 46 47 48 48 49 . . . . . . 52 52 52 52 52 53 55 7. Conclusiones 7.1. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 60 5. Implementación de la radio online 5.1. Introducción . . . . . . . . . . . . . 5.2. Preparando el servidor . . . . . . . 5.3. Streaming: Icecast2 . . . . . . . . . 5.3.1. Instalar Icecast2 . . . . . . . 5.3.2. Configurar Icecast2 . . . . . 5.4. Streaming: Ices . . . . . . . . . . . 5.4.1. Instalar Ices . . . . . . . . . 5.4.2. Configurar Ices . . . . . . . 5.4.3. Ices y Python . . . . . . . . 5.5. PHP y Wimpy Button . . . . . . . . 5.5.1. PHP básico . . . . . . . . . 5.5.2. Wimpy Button . . . . . . . 5.6. Usuarios y evaluaciones . . . . . . . 5.6.1. Creando cuentas de usuarios 5.6.2. Sistema de evaluación . . . . 5.6.3. Detectando oyentes . . . . . 5.7. Resumen de directorios y archivos . 5.7.1. Directorios . . . . . . . . . 5.7.2. Archivos . . . . . . . . . . . 6. Pruebas y resultados 6.1. Introducción . . . . . . 6.2. Pruebas al algoritmo . . 6.2.1. Matriz de 2x3 . 6.2.2. Matriz de 4x5 . 6.3. Tiempos del algoritmo 6.4. La radio online . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii 7.2. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3. Trabajo futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 61 Bibliografía 63 Anexo 64 ix Índice de tablas 1. 2. 3. 4. 5. 6. Hardware de Hopper . . . . . . . . . . . . Datos de Hopper . . . . . . . . . . . . . . . Cantidad de usuarios y evaluaciones . . . . Cantidad de evaluaciones por canción . . . Evaluaciones de los 6 usuarios . . . . . . . Cantidad de canciones evaluadas en común . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 55 56 57 57 x Índice de ecuaciones 1. 2. 3. 4. 5. 6. 7. 8. mds(x1 , x2 ) wt(x1 , x2 ) . S (x) . . . . D(x) . . . . w0S (x, y) . . w0D (x, y) . . w0 (x, y) . . . CP(t) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 13 14 15 15 15 15 xi Índice de figuras 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. Diagrama de funcionamiento de Ices y Icecast . . . Ejemplo: Evaluaciones del usuario 1 . . . . . . . . Ejemplo: Evaluaciones comunes entre dos usuarios Ejemplo: Predicciones para usuarios 1 y 2 . . . . . Radio sólo con reproductor . . . . . . . . . . . . . Página de inicio . . . . . . . . . . . . . . . . . . . Creando una nueva cuenta . . . . . . . . . . . . . Faltan datos para crear una cuenta . . . . . . . . . Errores creando una cuenta . . . . . . . . . . . . . Errores al ingresar . . . . . . . . . . . . . . . . . . Botones de opción y enviar . . . . . . . . . . . . . Páginas de inicio y últimas 20 . . . . . . . . . . . . Tiempos del algoritmo . . . . . . . . . . . . . . . Cuántas evaluaciones tiene cada canción . . . . . . Evaluaciones de 6 usuarios . . . . . . . . . . . . . Predicciones vs. evaluaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 12 13 14 43 44 44 45 45 45 46 48 54 55 56 58 xii Nomeclatura C ci E(u) D(x) i L(t) mds(x1 , x2 ) P(y) xi S (x) T (x) U ui V(x) W W0 w0 (x, y) w0D (x, y) w0S (x, y) WT wt(x1 , x2 ) : Universo de canciones en la radio. : Canción i. : Evaluaciones del usuario u. : Usuarios disímiles a x. : Índice. : Grupo de usuarios escuchando radio en tiempo t. : MDS entre usuario x1 y usuario x2 . : Grupo de usuarios que evaluaron la canción y. : Vector de evaluaciones del usuario i. : Usuarios similares a x. : Grupo de canciones evaluadas por usuario x. : Universo de usuarios de la radio. : Usuario i. : Vecindario de usuario x (Flycasting). : Matriz de evaluaciones de usuarios sobre canciones, sin predicción. : Matriz de evaluaciones de usuarios sobre canciones, con predicción. : Predicción de evaluación de usuario x sobre canción y. : Predicción usando grupo D(x). : Predicción usando grupo S (x). : Matriz de pesos entre usuarios. : Peso entre usuario x1 y usuario x2 . xiii Abreviaciones Mayúsculas AJAX DOM DTI HTML ID MD5 MP3 MSD PC PHP S.C. UDP URL XML : (Asynchronous JavaScript And XML) Técnica desarrollo web para aplicaciones interactivas. : (Document Object Model) Plataforma e interfaz neutral al lenguaje. : Dirección de Tecnologías de Información de la Universidad de Concepción. : (Hyper Text Markup Language) Lenguaje de marcado de hipertexto. : Identificador. : (Message-Digest Algorithm 5) Algoritmo de reducción criptográfico de 128 bits. : (MPEG-1 Audio Layer 3) Formato de compresión de audio digital. : (Mean Squared Distance) Distancia media cuadrática. : (Personal Computer) Computadora personal. : (PHP Hypertext Pre-processor) Lenguaje de programación interpretado. : (Source Client) Clientes generadores de streaming. : (User Datagram Protocol) Protocolo no orientado a la conexión. : (Universal Resource Locator) Dirección de internet. : (eXtensible Markup Language) Metalenguaje de etiquetas. 1 Capítulo 1. Introducción 1.1. Introducción general Con la masificación de internet, el mundo vive una gran revolución cultural, donde todos pueden acceder por distintas vías a una variada gama de contenidos. Entre estos está la música, arte donde las grandes casas discográficas han sido quienes dictaron tendencia en cuanto a lo que se oía y los artistas que triunfaban en el mundo musical. Sin embargo, con el surgimiento de las radios online, el abanico de opciones se ha ampliado, pues se puede escuchar emisoras de distintas ciudades y países del mundo, dando a los oyentes una diversidad de alternativas. Si bien lo anterior implica un beneficio, plantea el problema concreto de cómo encontrar lo buscado, ya que el contenido existente en internet es tan abundante que puede llegar a ser una tarea muy compleja. Esto se refleja en los distintos sitios de búsqueda que existen a nivel mundial y sus cada vez más avanzados algoritmos. Por esto se considera como una buena opción otro tipo de filtrado, el colaborativo, que se asemeja a las sugerencias que se hacen y reciben constantemente por las personas en sus relaciones interpersonales, definidas por los gustos históricos, vivencias y cultura. Además, permite no sólo buscar por contenido escrito, sino también por imágenes, sabores, intereses en general[3]. Así para facilitar el acceso a música que a la gente le gusta y conoce, pero más interesante aún, le gusta y desconoce [1], se puede usar el filtrado colaborativo para reconocer y agrupar a los usuarios de distintas maneras [4] [6], e implementar sistemas de recomendación en base a sus preferencias. Distintas maneras de realizar esto se han desarrollado a través del tiempo [1] [2] [5]. El trabajo que más se asemeja a lo planteado consiste en la implementación de un sistema que genera un listado en tiempo real, según el historial de petición de los usuarios, basado en los artistas solicitados y tratando de mantener cierta coherencia entre una canción y otra. Esto último se refiere a no cambiar de estilos musicales de manera drástica o aleatoria, mediante la identificación de artistas similares y, posteriormente, la restricción de los resultados posibles al rango “coherente”, lo que inevitablemente significa ignorar o posponer las proposiciones para un grupo minoritario. Mediante la presente memoria, se implementará una radio online que reciba los gustos de los usuarios conectados, identifique usuarios similares y disímiles, prediga evaluaciones por canción en base a ambos grupos y adapte la lista de reproducción para obtener una programación dinámica sin coherencia que cambie en línea de acuerdo a las preferencias de sus oyentes. 1.2. 1.2.1. Objetivos Objetivo general Desarrollar una radio online que adapte su programación al gusto de los usuarios conectados. 2 1.2.2. Objetivos específicos Implementar una radio online de reproducción en la web. Desarrollar un sistema de evaluación en línea para las canciones que escuchan los usuarios. Procesar las evaluaciones de los usuarios para predecir una programación adaptativa. 1.3. Alcances y limitaciones La programación dependerá de los gustos y preferencias de los usuarios que la estén escuchando, de tal manera que la mayoría quede conforme. Los usuarios no podrán cambiar directamente la lista de reproducción. No se le exigirá coherencia a la lista, ya que implementar esto significa dejar a algunos usuarios de lado para evitar cambios drásticos de estilos [2]. 1.4. Temario El temario se describe a continuación: El Capítulo 2 entrega la teoría del filtrado colaborativo, sus ventajas y desventajas, y plantea cómo será utilizado en este trabajo. El Capítulo 3 detalla tanto el hardware como el software a utilizar. El Capítulo 4 explica el desarrollo del algoritmo de predicción y cómo generar una lista adaptativa de canciones mejor evaluadas para una radio online. El Capítulo 5 cubre la implementación de la radio online, incluyendo el sistema de evaluación por parte de los usuarios y la obtención de una lista adaptativa. El Capítulo 6 muestra las pruebas y resultados hechos al sistema. El Capítulo 7 presenta las conclusiones finales y el posible trabajo futuro. 3 Capítulo 2. Filtrado colaborativo 2.1. Introducción El filtrado colaborativo es un tipo de recomendación personalizada basada en combinar las distintas opiniones de usuarios sobre diferentes productos de una misma temática, como música, libros, películas, entre otros, para luego separar a los usuarios similares y encontrar, gracias a sus preferencias, otros elementos conocidos o nuevos. Este sistema es muy útil cuando se tiene una gran variedad de opciones a elegir, tanto así que resulta imposible revisarlas todas. Cada usuario conoce y/o descubre una porción del total, y juntando todas esas preferencias se puede armar un arreglo de evaluaciones, para luego reconocer usuarios con intereses comunes y en base a ello proponer otras opciones no evaluadas y quizás desconocidas para cada uno. 2.2. Bases del filtrado colaborativo Ante una alta variedad de elementos de cierta índole, es necesario descubrir otro método de filtrar resultados y obtener de manera efectiva lo que se quiere. Para esto se desarrolla el filtrado colaborativo, el cual funciona en base a las opiniones y gustos de diferentes individuos, reconoce usuarios similares y entrega resultados recomendados por cada solicitud. Para que funcione correctamente, son necesarios tres pilares fundamentales [7]: 1. Deben participar muchas personas (mientras más participen, más probable encontrar usuarios similares). 2. Debe ser simple para la gente representar sus opiniones. 3. El algoritmo debe ser capaz de agrupar usuarios con intereses similares. Por lo tanto, la tarea de los usuarios es bastante sencilla, evaluar los elementos que presenta el sistema según sus preferencias. Sus evaluaciones serán usadas para aproximar el gusto de cada uno en ese tema, luego compararlas entre todos los demás y encontrar la vecindad cercana de usuarios similares. Finalmente el sistema recomienda elementos altamente evaluados por la vecindad, que el usuario no evaluó y quizás hasta no conoce. Además, si la recomendación no es correcta se puede evaluar negativamente, y así con el tiempo adquiere más exactitud para recomendar. Un punto clave es cómo combinar las preferencias y darles cierto peso para determinar las vecindades. Esto define cuántos usuarios las componen y cuán similares o cercanos son al usuario en cuestión. 4 2.3. Ventajas Una de las mayores ventajas que posee este tipo de filtrado, es el hecho de que cada recomendación es personalizada, al tener una vecindad de personas con gustos similares se puede hasta descubrir nuevos elementos que nunca hubieran sido considerados inicialmente. Además, las recomendaciones son una consecuencia para el usuario, no debe buscar ni similitudes con otros ni nuevos elementos, tan solo debe mantenerse evaluando y con el tiempo la recomendación será más exacta. De hecho, todos los usuarios hacen el mismo trabajo y reciben el mismo resultado. Esto se conoce como roles uniformes. Finalmente, tampoco es necesario interacción alguna entre las personas que usan el sistema, no importa si alguien conoce a miles o a nadie dentro de éste, mientras evalúen todos recibirán recomendaciones. 2.4. Desventajas Uno de los grandes problemas de este tipo de filtrado es el denominado inicio frío, mientras nadie evalúe es imposible comenzar a recomendar. Lo mismo ocurre cuando un item no ha sido evaluado, no se puede incluir en las recomendaciones, por lo tanto la novedad empieza a perder fuerza. Una manera de mejorar esto último es incluir esporádicamente dentro de las recomendaciones algún elemento sin evaluar, seleccionado por métodos probabilísticos o al azar, de tal manera que el usuario decida si es una buena o mala recomendación. Esto puede empeorar levemente los resultados, pero agrega cierta novedad aleatoria al sistema. Otro problema es cuando la cantidad de usuarios y evaluaciones es notoriamente menor frente a la cantidad de elementos a evaluar, lo que resulta en una baja cantidad de evaluaciones en común y un mal resultado como vecindad. Una manera efectiva de mejorar este problema es combinar el filtrado colaborativo con recomendaciones basadas en contenido. Por ejemplo, a un usuario le gusta una canción de John Lennon y a otro usuario una diferente. Usando sólo filtrado colaborativo, como ambas canciones son distintas, los usuarios no tienen nada en común. Pero si se analiza el contenido de esas canciones, se podría asociar que son del mismo intérprete, por lo tanto los usuarios pueden ser similares. Un efecto negativo a nivel de costo computacional es que las evaluaciones de los usuarios se almacenan en una matriz, y a grandes cantidades de elementos y usuarios se vuelve cada vez más dispersa. Distintas soluciones se plantean para este hecho, como descomponer la matriz, técnicas de reducción de orden, entre otras [5]. 5 2.5. Discusión En el presente trabajo se desarrollará un algoritmo que utilizará el filtrado colaborativo como principal herramienta para la predicción de nuevas canciones. Se plantea no sólo utilizar la información de usuarios similares para crear vecindades, sino que se puede agrupar a los disímiles también, y de tal manera generar más predicciones y obtener un listado de elementos mucho mayor con el cual recomendar. En cuanto a los problemas presentados, se incluirá en los resultados esporádicamente una canción aleatoria para agregar novedad. No se utilizará recomendaciones basadas en contenido, ni tampoco técnicas de descomposición para ahorrar costos de tiempo de cálculo, todo para observar el comportamiento de la nueva propuesta sin mayores mejoras, las cuales pueden ser estudiadas en un trabajo futuro. 6 Capítulo 3. Hardware y software 3.1. Introducción En este capítulo se describe tanto el hardware como el software a utilizar. Se trabaja con un sistema de servidor común donde se implementará la radio online. En tanto al software, pese a existir una gama de opciones para la implementación, se eligen estos programas por recomendaciones, simplicidad y uso masivo. 3.2. El servidor El PC usado como servidor consta de un procesador Pentium 4, y fue facilitado por el profesor guía. De sistema operativo se escoge Ubuntu Server 10.04 LTS por ser código abierto, sencillo y por contar con buen soporte. Algunas de las características del servidor se muestran en la tabla 1, y sus datos de configuración en la red de la universidad se muestran en la tabla 2. Tabla 1: Hardware de Hopper Procesador Velocidad Procesador Memoria RAM Discos Duros Intel(R) Pentium(R) 4 3.20 [GHz] 1 [GiB] DDR 33 [GiB] SCSI 33 [GiB] SCSI 149 [GiB] SATA Tabla 2: Datos de Hopper Dirección Web Dirección IP Máscara Puerta de Enlace DNS hopper.die.udec.cl 152.74.23.87 255.255.0.0 152.74.23.1 152.74.16.14 / 152.74.16.83 En él se almacenan todas las canciones en formato MP3 a utilizar para la radio, en el directorio /mnt/mp3/Radio/, ordenadas por artista y álbum. La dirección IP del servidor permite un acceso directo desde Internet a todo el mundo, pero el streaming de audio inicialmente queda limitado por el puerto 8000, el cual no está abierto fuera de la universidad por el DTI. De todas maneras, se solicita el desbloqueo del puerto para permitir el uso de la radio online a gente fuera del campus. 7 Figura 1: Diagrama de funcionamiento de Ices y Icecast 3.3. Software de streaming El streaming es la transmisión de un flujo continuo en el tiempo de datos de audio o vídeo, donde el receptor almacena cierta cantidad en una cola o buffer y reproduce este flujo a medida que lo recibe. Entonces, a diferencia del mecanismo de descarga del archivo, no se necesita tener todo el archivo para reproducirlo, a medida que se recibe se usa la información de manera fluida. Por lo mismo, no es necesario asegurar que todos los paquetes de datos llegan al destino, ya que al ser un flujo continuo, si se pierde un paquete se pueden seguir recibiendo los siguientes y así no detener la reproducción final. Si el paquete perdido arriba a destino en forma tardía, éste se descarta, ya que no tiene sentido adjuntarlo al flujo en reproducción. Por recomendación del profesor guía, se trabaja con el servidor de streaming llamado Icecast 2 y su cliente Ices 0.4. La estructura de trabajo para estos programas se puede observar en la figura 1. 3.3.1. Icecast 2 Este programa sirve para implementar un servidor de streaming de audio, el cual recibe dicho streaming de otros clientes (S.C.) y lo transmite a los usuarios. El sistema es mantenido por la Fundación Xiph.org. Es gratuito, su código es libre y soporta distintos tipos de formatos, Ogg Vorbis, MP3, AAC, entre otros [8]. Es una alternativa al programa propietario de servidor de medios SHOUTcast de Nullsoft. Se puede descargar de su página oficial Icecast.org. Los prerrequisitos software para instalar Icecast2 son: Biblioteca libxml2 - http://xmlsoft.org/downloads.html Biblioteca libxslt - http://xmlsoft.org/XSLT/downloads.html 8 curl - http://curl.haxx.se/download.html (>= versión 7.10). ogg/vorbis - http://www.vorbis.com/files (>= versión 1.0) En la sección 5.3 se detalla la instalación y configuración de este programa. 3.3.2. Ices Este cliente genera el streaming de audio enviado a Icecast2 para su transmisión. La razón de separar la generación del streaming del envío a sus usuarios propiamente tal, es para que el cliente pueda ser implementado en computadores con bajo ancho de banda, mientras el servidor lo sea en otro PC con mejores condiciones de transmisión. Así, el cliente sólo debe mandar el streaming una vez, y el servidor se encarga de reenviarlo a los demás. Existen dos versiones funcionales, la 0.4 (ices0) y la 2.0 (ices2). La principal diferencia es el soporte de formato de audio, ices0 sólo soporta MP3, mientras ices2 sólo soporta Ogg Vorbis. Por esta razón se escoge ices0, ya que el formato MP3 aún siendo privativo, es más masivo y permite encontrar mayor variedad. Los prerrequisitos software para instalar ices0 son: GCC y G++ - http://gcc.gnu.org/ LAME (mp3) - http://lame.sourceforge.net/ libshout - http://directory.fsf.org/wiki/Libshout Python (dev) - http://www.python.org (Opcional) Perl (dev) - http://www.perl.org (Opcional) Tanto Ices como Icecast usan archivos XML para su configuración. Una gran ventaja de este cliente es que permite añadirle funcionalidad mediante módulos en lenguajes Python o Perl para manejar el listado de canciones. En la sección 5.4 se detalla la instalación y configuración de este programa. 3.4. PHP y reproductor embebido web Para no discriminar a los usuarios por sus sistema operativo, programando un cliente de recepción streaming, se prefiere implementar la radio en formato página web. Primero se debe instalar PHP (http://www.php.net/) para poder montar una página web en el servidor. 9 Luego es necesario un sistema de reproducción online, para ello se elige trabajar con reproductores basados en flash de Adobe por ser masivos. Se escoge Wimpy Button por ser simple, configurable, bien documentado y compatible con Icecast 2 [9]. En la sección 5.5.2 se detalla la configuración e implementación de este reproductor. 10 Capítulo 4. Desarrollo de un algoritmo predictivo y listado adaptativo 4.1. Introducción En este capítulo se describe el desarrollo del algoritmo predictivo usando filtrado colaborativo, y cómo generar una lista adaptativa de canciones para una radio online. Los pasos a continuación son siguiendo el modelo de Flycasting [2], con algunas modificaciones. Este algoritmo se implementa usando el lenguaje de programación Python, por las razones expuestas en 3.3.2, y todas las funciones creadas se guardan en el archivo predictop.py. 4.2. Una radio online Primero es necesario definir las características de la radio donde se generará la lista. Una radio online cuenta con 2 entidades básicas: usuarios (U) y canciones (C). Los usuarios son personas que ingresan a la radio para escuchar música. Las canciones son archivos de audio usados para generar la lista de reproducción de la radio. Adicionalmente se puede agregar una tercera entidad, los artistas, pero en este caso no se trabajará con ello. U: Universo de usuarios. C: Universo de canciones. Cada usuario registrado puede evaluar la canción que esté siendo reproducida en el momento, y esa evaluación debe ser almacenada correctamente. Sólo existirá una evaluación por canción, pudiendo ésta ser modificada. E(u) son las evaluaciones del usuario u. E(u) : {c | c ∈ C ∧ u | u ∈ U ∧ u evaluó a c} Finalmente se debe saber en todo momento quienes son los usuarios conectados escuchando la radio. L(t) es el grupo de oyentes en un tiempo t. L(t) : {u | u ∈ U ∧ u está escuchando en el tiempo t} 4.3. Generar la lista El objetivo final es generar una lista de reproducción que satisfaga a la mayor cantidad de usuarios conectados en un momento dado. Al no exigirle coherencia de estilos al cambiar de una canción a otra, no es necesario determinar artistas similares. Esta lista debe ser capaz de cambiar a medida que entran y salen los oyentes, para adaptarse a las nuevas preferencias. Para esto se deben cumplir las siguientes etapas: 11 1. Adaptar las evaluaciones. 2. Predecir la nota de las canciones que los usuarios no han evaluado. 3. Agrupar las canciones que tengan una evaluación promedio aceptable, según los actuales oyentes. 4. Seleccionar la nueva canción a reproducir. 4.3.1. Adaptar evaluaciones Como hay distintas maneras de evaluar las canciones (escalas, ponderaciones, etc.), es necesario traducir a valores aptos para seguir trabajando con ellos. Esta etapa es la menos rígida de todas, ya que depende mucho de cómo se decida implementar el sistema de evaluaciones. En el caso de este trabajo, las evaluaciones posibles son del 1 al 5, siendo 1 la peor evaluación y 5 la mejor. El objetivo es ordenar usuarios vs. canciones y sus evaluaciones traducidas al rango [0, 1], siendo 0 no evaluada y 1 la mejor evaluación, de tal manera que se puedan distinguir cuales han sido ingresadas por los usuarios y cuáles hay que predecir. Sea W la matriz de orden n × m, donde n es la cantidad de usuarios evaluadores y m las canciones evaluadas. W ∈ Mn×m (R) n: N de usuarios evaluadores. m: N◦ de canciones evaluadas. ◦ 4.3.2. Predicción de evaluaciones Con los datos en la matriz W se puede obtener un listado de las canciones mejor evaluadas para elegir la siguiente a reproducir, pero usando filtrado colaborativo se puede extraer mucha información adicional para una mejor elección. Esto se logra prediciendo la mayoría de las evaluaciones que fueron traducidas a 0, las que no han sido ingresadas por los usuarios. Hay distintas maneras de aplicar el filtrado colaborativo. En este caso se sigue la metodología de Flycasting [2], usar el algoritmo de la diferencia cuadrática media o Mean Squared Difference (MSD), junto con el algoritmo de correlación de Pearson. Antes de detallar los algoritmos, es necesario definir 2 nuevos conjuntos. T (x) es el grupo de canciones que han sido evaluadas por el usuario x. T (x) : {c | c ∈ C ∧ W xc > 0} P(y) es el grupo de usuarios que han evaluado la canción y. P(y) : {u | u ∈ U ∧ Wuy > 0} 12 Figura 2: Ejemplo: Evaluaciones del usuario 1. En la figura 2 se muestra un ejemplo de conjunto T (1), donde el usuario 1 ha evaluado 6 de las 8 canciones disponibles. Los discos representan distintas canciones, las flechas azules corresponden a nota máxima, y las rojas a nota mínima. Con esto se puede definir el algoritmo MDS. El primer paso es determinar cuán similares son dos usuarios x1 y x2 . La similitud se mide como la media de las diferencias cuadráticas de las evaluaciones que ambos usuarios han hecho sobre las mismas canciones. Sea mds(x1 , x2 ) la diferencia cuadrática media entre los usuarios x1 y x2 . Si |T (x1 ) ∩ T (x2 )| > 0, se tiene P mds(x1 , x2 ) : (W x1 i − W x2 i )2 |T (x1 ) ∩ T (x2 )| i∈T (x1 )∩T (x2 ) (1) Si |T (x1 ) ∩ T (x2 )| = 0, entonces ambos usuarios no han evaluado canciones en común, por tanto mds(x1 , x2 ) debe ser 1, el valor más alto posible. Se puede apreciar que 0 ≤ mds(x1 , x2 ) ≤ 1. En la figura 3 se continúa el ejemplo anterior, mostrando las evaluaciones realizadas por el usuario 2 y las comunes entre ambos usuarios. |T (x1 ) ∩ T (x2 )| valdría 4. El segundo paso del algoritmo es calcular los pesos, wt(x1 , x2 ), los que representan cuán similar es el usuario x2 al usuario x1 . wt(x1 , x2 ) : 1 − mds(x1 , x2 ) (2) Se puede notar que 0 ≤ wt(x1 , x2 ) ≤ 1. Con wt(x1 , x2 ) = 1 significa que ambos usuarios son idénticos, y mientras wt(x1 , x2 ) sea alto, significa que los usuarios son más parecidos. Así mismo, wt(x1 , x2 ) más bajo significa que su similitud disminuye, y si wt(x1 , x2 ) = 0 significa que no tienen canciones evaluadas en común para comparar. Con estos pesos se arma la matriz de pesos WT . 13 Figura 3: Ejemplo: Evaluaciones comunes entre dos usuarios. WT ∈ Mn×n ([0, 1]) | WT xy = wt(x, y) n: Cantidad de usuarios. Es útil notar que el valor mínimo que se puede calcular como peso para este caso es de 0,36, ya que la nota mínima es 0,2 y la más alta es 1 normalizada. Suponiendo que no hay más coincidencias, usando MDS resulta 0,82 = 0,64 y el peso será 1 − 0,64 = 0,36. Del ejemplo, como todas las evaluaciones en común entre los usuarios 1 y 2 son contrarias, el peso entre ellos sería el mínimo, 0,36. Lo siguiente es determinar cómo usar los pesos y evaluaciones para predecir las canciones no ingresadas. Flycasting [2] propone definir un vecindario para cada usuario x que sea útil para la predicción. Este vecindario consiste en todos los otros usuarios u tal que WT xu sea mayor o igual a un umbral Φ. El vecindario V(x) queda conformado por la cantidad de usuarios suficientemente similares a x para predecir las evaluaciones que faltan. Entonces, V(x) se define como V(x) : {u | u ∈ U ∧ WT xu ≥ Φ} En este trabajo, se propone modificar esta manera de predecir, ya que puede ocurrir el caso que ningún peso fuese mayor al umbral Φ, por lo tanto V(x) = 0, x no tendría usuarios similares y no se le podría predecir ninguna evaluación adicional, lo que disminuye notablemente el conjunto de posibles canciones a reproducir. Por lo tanto, se plantea que para cada usuario se separen los demás en dos categorías, similares y disímiles. Los usuarios similares son los mismos definidos anteriormente como V(x), y todos los que no queden en este grupo están entonces en la categoría de disímiles. Sean S (x) los usuarios similares y D(x) los disímiles. S (x) : {u | u ∈ U ∧ WT xu ≥ Φ} (3) 14 Figura 4: Ejemplo: Predicciones para usuarios 1 y 2 D(x) : {u | u ∈ U ∧ WT xu < Φ ∧ WT xu , 0} (4) De esta manera se obtiene más información. Mientras x tenga pesos no nulos, si no hay una vecindad de usuarios similares a x deberá existir una vecindad de disímiles, por tanto se puede predecir de manera inversa. Esto garantiza que mientras dos usuarios tengan una canción evaluada en común, se podrá realizar predicción. Esta proposición se basa en el supuesto que a las personas normalmente le gusta y disgustan ciertos estilos definidos. Por ejemplo, a un amante de la música clásica normalmente no le gusta la música tropical, o alguien que disfruta del pop es poco probable que escuche metal gótico. Se reconoce que puede ser muy drástica la separación entre usuarios similares y disímiles solamente, y probablemente sea necesario definir otros niveles entremedio, pero se considera éste un buen punto de partida. En la figura 4 se continúa con el ejemplo, ilustrando con flechas entrecortadas las predicciones para cada usuario. Siguiendo el método de Flycasting, al tener únicamente evaluaciones opuestas, el peso entre los usuarios sería muy bajo, por lo tanto, no podría predecir absolutamente nada con esta información. Según el método propuesto, se reconocería a los usuarios como disímiles y se lograría predecir la nota correspondiente. Para predecir la evaluación de un usuario x a una canción y se usa el promedio ponderado de las notas de los demás usuarios que han evaluado y. Claramente si x evaluó y, se usa esa nota en vez de una predicción. Para esto, se deben obtener primero las predicciones por separado usando S (x) y D(x). Sea w0S (x, y) la predicción del usuario x para la canción y usando a los usuarios similares S (x) P w0S (x, y) : (WT xi · Wiy ) i∈S (x)∩P(y) WT xi i∈(S (x)∩P(y)) P (5) 15 Sea w0D (x, y) la predicción del usuario x para la canción y usando a los usuarios distintos D(x). Rtop es una constante que se ajusta según la escala de evaluación, normalmente la nota siguiente a la máxima, para que al restarle una nota normal se obtenga la inversa. En este caso sería 6, como la escala es de 1 a 5. P i∈(D(x)∩P(y)) (WT xi · Wiy ) 0 P (6) wD (x, y) : Rtop − i∈D(x)∩P(y) WT xi Rtop : Constante para invertir evaluación. De esta manera, los usuarios del grupo S (x) predicen una evaluación, y los del grupo D(x) predicen otra. Para juntar ambas predicciones se vuelve a usar el promedio ponderado, siendo la cantidad de usuarios en ambos grupos la ponderación. Sea NS el número de usuarios en S (x) y ND el número de usuarios en D(x), y w0 (x, y) la predicción final de x sobre y. Entonces, se tiene w0 (x, y) : (w0S (x, y) · NS ) + (w0D (x, y) · ND ) NS + ND (7) Nótese que si NS + ND = 0, ese usuario no tendría ninguna canción evaluada en común con otro y no se podrían predecir sus evaluaciones que faltan. Con este resultado, se obtiene la matriz de predicción W 0 tal que 0 W 0 ∈ Mn×m (R) | W xy = w0 (x, y) n: Cantidad de usuarios. m: Cantidad de canciones. Esta matriz W 0 contiene las evaluaciones de los usuarios y las predicciones. Por lo tanto, se tiene toda la información posible para el paso siguiente, generar un listado de canciones populares. 4.3.3. Las más populares En esta etapa se usa la matriz de predicción W 0 . Esta matriz es calculada para estimar qué canciones son las que más le gustan a cada usuario. Las canciones populares son consideradas como las que tienen una evaluación promedio sobre un umbral ω. Recordando que L(t) son los usuarios conectados, se tiene CP(t) como el conjunto de canciones populares, ambos en el tiempo t. CP(t) : y | y ∈ C ∧ P i∈L(t) Wiy0 |L(t)| >ω (8) 16 Nótese que L(t) = 0 significa que no hay usuarios escuchando la radio en ese momento, por tanto no tiene sentido tratar de elaborar un listado de canciones populares. Importa el hecho que si un oyente no ha evaluado, pertenece a L(t) pero no aporta con canciones sobre el umbral, por lo que escuchará lo que le guste a los demás o cualquier canción. 4.3.4. La próxima canción Ya teniendo un listado de canciones populares, se puede determinar la siguiente a reproducir. Una manera sencilla de seleccionar la siguiente es elegir la primera canción, o bien cualquiera de este listado, e introducirla al final para que sea una reproducción cíclica y sólo cambie de acuerdo a los usuarios conectados. El principal problema de este modelo es que las populares empezarían a repetirse, las menos populares nunca sonarían en la radio, y peor aún, no habrían canciones nuevas en las predicciones, ya que si no fueron evaluadas antes de iniciar el sistema, nunca serán reproducidas. Para solucionar esto último se pueden agrupar las canciones que nunca han sido evaluadas, y cada cierto tiempo reproducir una de estas mediante un sistema probabilístico, de tal manera que algún usuario evalúe dicha canción desconocida y así ésta entre al sistema. Con respecto a las canciones repetitivas y las menos populares (bajo el umbral ω), se les podría agregar un peso o cuenta a todas, para así limitar la cantidad excesiva de repeticiones y subir de nivel a alguna menos popular cada cierto tiempo. También se puede generar un listado de las últimas N reproducidas, y así evitar muchas repeticiones. 4.4. El algoritmo en Python En esta sección se explica cómo se desarrolla el algoritmo predictivo y la generación de la lista adaptativa en Python, para que se pueda importar directamente desde el módulo mencionado en la sección 3.3.2. Las funciones se escriben dentro del archivo preditop.py, y a continuación se describe el funcionamiento de cada una. 4.4.1. Función create_matrix En esta función se crea de manera recursiva una matriz de orden CxF, siendo C el número de columnas y F el de filas. Recibe los siguientes argumentos: shape: Tupla de forma (C, F), siendo C la cantidad de columnas y F de filas. value: Valor inicial de los elementos de la matriz. Si se omite toma valor 0. 17 def create_matrix(shape, value = 0): if not shape: return value return [create_matrix(shape[1:], value) for _ in xrange(shape[0])] 4.4.2. Función MDS Antes de definir la función, es necesario explicar lo siguiente: Python posee un tipo de estructuras llamadas diccionarios, similares a una lista, pero que no ordenan secuencialmente los elementos guardados, y tampoco poseen índices para acceder a un dato, sino más bien se acceden mediante una clave que puede ser entero, cadena de texto, entre otros. Hecha la aclaración, en esta función se calcula la diferencia cuadrática media (4.3.2) entre diccionarios de dos usuarios, donde las claves son el índice de la canción y el resultado es la evaluación de cada una. Dichas evaluaciones no deben estar normalizadas. Recibe los siguientes argumentos: x1 : Diccionario de usuario 1. x2 : Diccionario de usuario 2. norm: Flotante usado para normalizar valores. Si se omite toma valor 5.0. Si la canción en x1 tiene una evaluación (distinta a cero) y x2 también, entonces se calcula la potencia de las diferencias normalizadas y se suma a la diferencia total. Cuando se termina de revisar el diccionario, si no hubo canciones evaluadas en común se debe retornar 1.0, y si hubo se retorna la división entre las potencias acumuladas y la cantidad de coincidencias. def MDS(x1, x2, norm = 5.0): enambos = 0.0 sdif = 0.0 for track in x1: if x1[track] != 0 and x2[track] != 0: sdif += pow(x1[track]/float(norm) - x2[track]/float(norm), 2) enambos += 1.0 if enambos == 0: return 1.0 else: return sdif/enambos 18 4.4.3. Función CalculaPesos En esta función se calcula y retorna la matriz cuadrada de pesos de orden N, definida en la sección 4.3.2 (WT ). N es la cantidad de usuarios que han evaluado alguna canción. Recibe sólo un argumento: X: Lista de diccionarios de índice de canción y evaluación de los usuarios. Primero se crea la matriz cuadrada de orden N, siendo N la cantidad de usuarios con alguna evaluación, e inicializada en 1. Luego, por cada diccionario en la lista, se revisa cada diccionario en la lista y, adicionalmente, crea los índices i y j por cada uno. Si son el mismo diccionario (i = j) no es necesario calcular el peso. En vez de calcular un peso de la triangular inferior ( j > i), se copia de la triangular superior, y en otro caso (i > j) se calcula el peso. Esto se puede realizar ya que la matriz de pesos por definición es simétrica, y siempre se calcula primero el peso a copiar de la triangular superior. def CalculaPesos(X): PesosPrediccion = create_matrix((len(X), len(X)), 1) for i, inlista in enumerate(X): for j, dicts in enumerate(X): if i == j: continue elif PesosPrediccion[j][i] != 1: PesosPrediccion[i][j] = PesosPrediccion[j][i] else: PesosPrediccion[i][j] = 1 - MDS(inlista, dicts, 5.0) return PesosPrediccion 4.4.4. Función Predict En esta función se predicen las evaluaciones que los usuarios no han ingresado en base a todas las canciones que han sido evaluadas. Recibe los siguientes argumentos: X: Lista de diccionarios de índice de canción y evaluación de los usuarios. P: Matriz de pesos de orden N, donde N es la cantidad de usuarios que han evaluado alguna canción. U: Umbral de separación que define usuarios similares y disímiles. Si se omite toma valor 0.5. 19 Es necesario definir algunas variables antes de iniciar la predicción. En POSnum y POSden se calcula el numerador y denominador de la predicción con todos los usuarios similares, el conjunto S (x). En NEGnum y NEGden se calcula el numerador y denominador de la predicción con todos los usuarios disímiles, el conjunto D(x). En nPOS y nNEG se cuenta la cantidad de usuarios similares y disímiles respectivamente. Además, para ir guardando las predicciones calculadas y no intervenir en el cálculo de las que faltan, es necesario crear una copia completa de X, y esta copia se retorna al final de la función. Para realizar dicha copia se necesita importar el módulo copy. def Predict(X, P, U = 0.5): POSnum = 0.0 POSden = 0.0 NEGnum = 0.0 NEGden = 0.0 nPOS = 0 nNEG = 0 Xfinal = copy.deepcopy(X) ... Luego se crea un índice i y por cada diccionario Xdict en la lista X, por cada evaluación presente, si es distinta a cero significa que ya fue evaluada, por lo tanto no se cambia. def Predict(X, P, U = 0.5): ... for i, Xdict in enumerate(X): for inXdict in Xdict: if Xdict[inXdict] != 0: continue else: ... Si la evaluación es cero, significa que no ha sido ingresada y es necesario predecir. Entonces se crea un índice j y por cada peso del usuario de diccionario Xdict, si i=j significa que es el mismo usuario, no tiene sentido una predicción auto referente. Si el peso es menor a 0.2 significa que los usuarios no tienen evaluaciones en común (recordar que el menor peso posible es 0.36, dos usuarios completamente disímiles, luego pasa a 0 cuando no hay nada en común). 20 Si el peso es mayor al umbral, y el usuario que sirve de referencia a la predicción evaluó la canción en cuestión, entonces se le suma a POSnum y POSden su cálculo correspondiente y se cuenta un usuario similar. Lo mismo ocurre si el peso es menor que el umbral, pero a NEGnum y NEGden respectivamente. En cualquier otro caso, como que exista peso pero no evaluación con qué predecir, se sigue buscando. def Predict(X, P, U = 0.5): ... for i, Xdict in enumerate(X): ... else: for j,ipesos in enumerate(P[i]): if i == j: continue elif ipesos < 0.2: continue elif ipesos >= U and X[j][inXdict] != 0: POSnum += ipesos*X[j][inXdict] POSden += ipesos nPOS += 1 elif ipesos < U and X[j][inXdict] != 0: NEGnum += ipesos*X[j][inXdict] NEGden += ipesos nNEG += 1 else: continue ... Después de revisar las posibles combinaciones con los pesos y las evaluaciones de referencia, es necesario asegurar que la división de numeradores y denominadores sea posible. Luego, si hubo coincidencia para calcular predicción, se realiza el cálculo descrito en la sección 4.3.2. 21 def Predict(X, P, U = 0.5): ... else: ... if POSden < 0.2: # peso menor 0.36 POSdiv = POSnum else: POSdiv = POSnum/POSden if NEGden < 0.2: # peso menor 0.36 NEGdiv = NEGnum else: NEGdiv = NEGnum/NEGden if nPOS == 0 and nNEG == 0: predicha = 0 else: predicha = (POSdiv*nPOS+(6-NEGdiv)*nNEG)/(nPOS+nNEG) ... Finalmente, se actualiza la evaluación en cuestión con su predicción y las variables se igualan a cero. Al terminar la predicción para todas las evaluaciones no realizadas, se retorna la nueva lista con diccionario de evaluaciones y predicciones. def Predict(X, P, U = 0.5): ... else: ... Xfinal[i][inXdict] = predicha POSnum = 0.0 POSden = 0.0 NEGnum = 0.0 NEGden = 0.0 nPOS = 0 nNEG = 0 return Xfinal 22 4.4.5. Función GetPredicciones En esta función se importan las evaluaciones de todos los usuarios, se ordenan en una lista de diccionarios, se llama a las funciones anteriores para obtener las predicciones y se retorna el resultado. Recibe un solo argumento: rutausers: Cadena de texto indicando la ruta donde se encuentran los archivos de los usuarios. Los archivos de usuarios deben tiene la estructura definida en 5.6.1. Según los pasos explicados en la sección 4.3, el primer objetivo es ordenar los usuarios y todas sus evaluaciones. En 4.3.1 se recomienda hacerlo en una matriz W de orden n × m, donde n es la cantidad de usuarios evaluadores y m las canciones evaluadas. Por comodidad se decide no implementar W como una matriz, sino más bien como un diccionario de usuarios y sus evaluaciones, donde estas quedan agrupadas en otro diccionario de índice de canción y evaluación. Para esto se debe obtener todas las evaluaciones de todos los usuarios, se logra recorriendo el directorio donde se almacenan todos los usuarios (/var/www/.usuarios/), y se crea un listado llamado allusers con todos los nombres de usuarios que existen. Para poder recorrer un directorio es necesario importar el módulo os. def GetPredicciones(rutausers): allusers = [] for root,dirs,files in os.walk(rutausers): for file in [f for f in files]: allusers.append(file) Luego se crean dos listados vacíos, uno llamado listaUsersEval para agregar todos los usuarios que han evaluado alguna canción, y listadict para ordenar los diccionarios de índice canción y evaluación que se van creando. Luego por cada usuario listado en allusers se abre su archivo, se descarta su clave y correo electrónico, y si aún quedan datos por extraer, se agrega dicho usuario a listaUsersEval. 23 def GetPredicciones(rutausers): ... listaUsersEval = [] listadict = [] for user in allusers: fu = open(rutausers+user, ’r’) evals = fu.read() fu.close() evals = evals.split("\n") evals.pop() # Borra ultimo elemento nulo evals.pop(0) # Borra Password evals.pop(0) # Borra Correo if (len(evals) == 0): continue else: listaUsersEval.append(user) ... Ahora se sigue trabajando sólo con los usuarios que han evaluado alguna canción. El contenido leído sigue siendo una lista de cadenas de texto con formato índice evaluación, por lo que es necesario cambiarla a una lista de tupla de dos enteros, para luego transformarla a un diccionario llamado dictevals, donde las claves sean los índices de las canciones y el contenido sean las evaluaciones. def GetPredicciones(rutausers): ... for user in allusers: ... for index, pareval in enumerate(evals): evals[index] = tuple(int(i) for i in pareval.split(" ")) dictevals = dict(evals) ... 24 Es necesario que todos los diccionarios tengan la mismas claves (índices de canciones). Si algún usuario no ha evaluado la canción, entonces ésta queda registrada con un cero para saber que se necesita predecir dicha evaluación. Esto se realiza de la siguiente manera: 1. Se genera el nuevo diccionario en dictevals. 2. Por cada índice presente en el diccionario, si no se encuentra en algún diccionario listado en listadict, se agrega con nota cero. 3. Por cada índice presente en cada diccionario listado en listadict, si no se encuentra en dictevals, se agrega con nota cero. 4. Se agrega dictevals a la lista listadict. Esto se repite hasta que no hayan usuarios con evaluaciones. def GetPredicciones(rutausers): ... for user in allusers: ... dictevals = dict(evals) for inlista in listadict: for indict in dictevals: if not indict in inlista: inlista.setdefault(indict,0) for ininlista in inlista: if not ininlista in dictevals: dictevals.setdefault(ininlista,0) listadict.append(dictevals) ... Teniendo el listado de diccionarios de índices de canciones y evaluaciones, y con ceros donde se necesita predecir, se puede llamar a la función CalculaPesos, para luego llamar a Predict con un umbral medio Φ de 0.7 (el máximo peso es 1 y el mínimo 0.36). Finalmente se crea un diccionario llamado dictUsersEvals para asociar a los nombres de usuarios con sus diccionarios de índices y evaluaciones, y luego se retorna como resultado final. 25 def GetPredicciones(rutausers): ... PesosPrediccion = CalculaPesos(listadict) listadict = Predict(listadict, PesosPrediccion, 0.7) dictUsersEvals = {} for i,dicc in enumerate(listadict): dictUsersEvals[listaUsersEval[i]] = dicc return dictUsersEvals 4.4.6. Función TopTracks En esta función se genera un listado de todas las canciones evaluadas o predichas para ciertos usuarios sobre un umbral. Recibe tres argumentos: Pred: Diccionario de usuarios con diccionarios de índices de canciones y evaluaciones o predicciones (se obtiene de GetPredicciones). Users: Lista de usuarios. Tol: Nota mínima que deben tener las canciones. Si se omite toma valor 3. La lista Users debe contener a los usuarios conectados escuchando la radio. Lo primero es revisar que si no hay usuarios en la lista, la función retorna falso (False) automáticamente. De lo contrario genera otra lista llamada listtracks con todas las canciones predichas para todos los usuarios en Users. def TopTracks(Pred, Users, Tol = 3): if (len(Users) == 0): return False listtracks = [] for user in Pred: for track in Pred[user]: if track not in listtracks: listtracks.append(track) ... Luego, se tiene una variable para calcular la media, otra para contar cuántos usuarios aportan a la media y una lista vacía para añadir las canciones que superen el umbral. Por cada canción presente 26 en listtracks, y por cada usuario listado en Users, si el usuario no se encuentra en Pred, significa que no ha evaluado canción alguna, por lo tanto no afecta al listado final. De lo contrario, se suma la nota a la media y se aumenta la cuenta de usuarios. Se divide la acumulación de notas por la cantidad de usuarios para obtener la media, y si supera el umbral, se agrega al listado final, e independiente de eso se igualan a cero las variables mencionadas. Finalmente se retorna el listado resultante. def TopTracks(Pred, Users, Tol = 3): ... media = 0 uTotal = 0 listTop = [] for track in listtracks: for uname in Users: if uname not in Pred: continue userdict = Pred[uname] media += userdict[track] uTotal += 1 media /= uTotal if (media >= Tol): listTop.append(track) media = 0 uTotal = 0 return listTop 4.4.7. Función PredictTop En esta función se usan todas las anteriores. Se une el resultado de GetPredicciones con el de TopTracks y se retornan en una lista pareada. Además, se puede escribir todo el arreglo de predicciones a un archivo externo. Recibe cuatro argumentos: rutaonline: Ruta del archivo que contiene lista de usuarios conectados escuchando la radio. rutausers: Ruta del directorio que contiene los archivos de usuarios. tol: Nota mínima usada como tolerancia. Si se omite toma valor 3. rutaPtoF: Ruta del archivo donde escribir arreglo de predicciones. Si se omite queda como una cadena de texto vacía. 27 Primero se obtienen las predicciones y se guardan en prediccion. Si se ingresa una ruta en rutaPtoF, se abre el archivo, se borra su contenido y se escriben las predicciones siguiendo el formato especificado: Username es el nombre de cada usuario, track el índice de cada canción y pred la predicción para cada una. Username1:track11=pred11;track12=pred12;... Username2:track21=pred21;track22=pred22;... Luego se abre el archivo de rutaonline, se lee su contenido, se genera el listado de usuarios conectados escuchando la radio, y finalmente se retorna el resultado de la predicción y el listado de TopTracks en una lista de dos elementos (diccionario y lista). def PredictTop(rutaonline, rutausers, tol=3, rutaPtoF = ""): prediccion = GetPredicciones(rutausers) if(rutaPtoF != ""): fPtoF = open(rutaPtoF, ’w’) for user in prediccion: fPtoF.write(user+":") for track in prediccion[user]: fPtoF.write(str(track)+’=’+str(prediccion[user][track])+’;’) fPtoF.write("\n") fPtoF.close() fonlineusers = open(rutaonline, ’r’) users = fonlineusers.read() fonlineusers.close() users = users.split("\n") users.pop() return [prediccion, TopTracks(prediccion, users, tol)] Con estas funciones se puede predecir y obtener un listado adaptativo de canciones. Las pruebas realizadas al algoritmo se pueden ver en la sección 6.2. 28 Capítulo 5. Implementación de la radio online 5.1. Introducción En este capítulo se explica la implementación de la radio online, desde la preparación del servidor, los programas para generar y transmitir el streaming de audio, el sistema de evaluación de los usuarios y cómo aplicar el algoritmo predictivo para generar una lista adaptativa. 5.2. Preparando el servidor El servidor tiene instalado el sistema operativo Ubuntu Server 10.04 LTS, por lo tanto cuenta con un gestor de paquetes que permite instalar de manera rápida y eficiente la mayoría de los programas necesarios, hasta se encarga de instalar dependencias y/o prerrequisitos. Se puede usar por línea de comandos o en su interfaz gráfica. Se prefiere línea de comandos ya que el manejo de paquetes y dependencias es más trasparente. Para instalar los programas, se necesita permiso de administrador (root) o súper usuario, y el gestor se invoca mediante el comando sudo apt-get install PROGRAMA PROGRAMA sería el programa a instalar. El comando retorna si existe dentro de los registros para instalarlo directamente, y de ser así, entonces sólo queda aceptar la instalación. Si no está disponible para ésta distribución de Ubuntu, entonces es necesario bajar los archivos fuente. Para la segunda opción es necesario contar con el comando make, el cual se puede descargar e instalar usando el gestor, de la manera sudo apt-get install make Luego, para instalar programas usando make se deben seguir los siguientes pasos: 1. cd RUTA 2. ./configure 3. make 4. make install RUTA sería la ruta a los archivos fuente descargados. Algunos programas pueden requerir realizar la llamada a make como súper usuario. Además, normalmente los archivos fuente vienen comprimidos en un archivo .tar.gz. Para descomprimirlos se debe usar el siguiente comando 29 tar xvzf archivo.tar.gz Esto cubre cómo instalar los programas en el servidor. A continuación se describe la instalación de los programas a usar, su configuración, y la implementación de la página web de la radio online. 5.3. Streaming: Icecast2 5.3.1. Instalar Icecast2 Como se detalla en la sección 3.3.1, este programa tiene varios prerrequisitos, pero afortunadamente Icacast2 se encuentra dentro de los repositorios para Ubuntu Server 10.04 LTS, por lo tanto se puede instalar usando el gestor de paquetes de la siguiente manera sudo apt-get install icecast2 5.3.2. Configurar Icecast2 Teniendo Icecast2 instalado, es necesario realizar su configuración. Esto se hace modificando un archivo XML tipo, el cual se encuentra en la ruta /etc/icecast2/icecast.xml. El archivo consta de todas las posibles opciones que tiene el programa, siendo las básicas las siguientes: <icecast> <limits> ... </limits> <authentication> ... </authentication> <hostname>...</hostname> <listen-socket> ... </listen-socket> <fileserve>1</fileserve> <paths> ... </paths> <logging> ... </logging> <security> ... </security> </icecast> limits: Define la cantidad máxima de conexiones fuentes (S.C.) y clientes, y parámetros de conexión. No necesita ser modificado. authentication: Define las claves de conexión para fuente (source, S.C.), relay (para logs, no usado), y nombre y contraseña para administrador. hostname: Nombre del servidor de streaming. listen-socket: El socket (puerto) donde emitir streaming. No necesita ser modificado. 30 fileserve: Define si se puede servir archivos estáticos. No necesita ser modificado. paths: Rutas a archivos de registro de conexión, solicitud de archivos estáticos, de administración, entre otros. No necesita ser modificado. logging: Nombre de los archivos de registro de acceso y errores, y qué tipo de eventos registrar. No necesita ser modificado. security: Agregar seguridad al servidor usando chroot. No necesita ser modificado. Como se puede apreciar, la mayoría de los valores por omisión son suficientes para configurar correctamente el servidor. Lo que sí se recomienda cambiar es la clave de conexión para fuentes y el usuario y contraseña del administrador. <authentication> <!-- Sources log in with username ’source’ --> <source-password>claveCliente</source-password> <!-- Relays log in username ’relay’ --> <relay-password>relay</relay-password> <!-- Admin logs in with the username given below --> <admin-user>Administrador</admin-user> <admin-password>claveAdmin</admin-password> </authentication> claveCliente sería la clave para usuarios fuentes (S.C.), Administrador el nombre del usuario administrador y claveAdmin la clave del administrador. La clave para usuarios relay carece de importancia, ya que no es usada actualmente. La ruta donde se crearán los archivos de registro por defecto (/var/log/icecast2) puede causar problemas. Para esto hay dos posibles soluciones: cambiar la ruta o cambiar los permisos del directorio. Para mantener el orden de los archivos de registro dentro del sistema operativo, se prefiere cambiar los permisos. Finalmente, para levantar el servidor se llama a icecast2 por línea de comando, -c para especificar la ruta del archivo XML de configuración y -b para ejecutarlo separado a la consola. icecast2 -c /etc/icecast2/icecast.xml -b 31 5.4. Streaming: Ices 5.4.1. Instalar Ices Como se describe en la sección 3.3.2, la versión de Ices que reproduce archivos MP3 es ices0. Esta no se encuentra disponible en los repositorios, por lo que no se puede usar el gestor de descargas para instalarlo. Por tanto, es necesario descargar los archivos fuente de http://icecast.org/ices.php. Antes de instalarlo, se deben tener resueltas todas las dependencias. Casi todas se pueden obtener usando el gestor de instalación. sudo apt-get install gcc g++ sudo apt-get install libshout3-dev sudo apt-get install python-dev LAME se debe instalar descargando los archivos fuente: http://lame.sourceforge.net/. Teniendo las dependencias listas, se procede a instalar ices0 pero agregándole dos opciones. La primera es with-python para poder usar Python para manejar la lista de reproducción, y la segunda es with-lame para acceder a la metadata de los archivos MP3. Estas opciones se agregan en la etapa de configuración. ./configure with-python with-lame 5.4.2. Configurar Ices Una vez instalado ices0, es necesario configurarlo de la misma manera que Icecast2, editando un archivo XML de configuración, el cual se encuentra en la ruta /usr/local/etc/ices.conf.dist. En él se pueden configurar todas las opciones del programa. Las básicas son: <ices:Configuration xmlns:ices="http://www.icecast.org/projects/ices"> <Playlist> ... </Playlist> <Execution> ... </Execution> <Stream> ... </Stream> </ices:Configuration> Playlist: Define si la lista de reproducción será manejada por ices0, por módulos de Python o Perl. Por defecto la maneja ices0. Execution: Define el comportamiento del programa. Stream: Datos de conexión y características del stream. Los valores por defectos no son suficientes para configurar un stream avanzado. A continuación se detallan las opciones de cada marco XML. 32 <Playlist> <File>playlist.txt</File> <Randomize>1</Randomize> <Type>builtin</Type> <Module>ices</Module> </Playlist> File: Ruta a archivo que contiene lista de rutas de canciones a reproducir (playlist). [builtin]. Randomize: Si se desea reproducir aleatoria la lista; 0: No, 1: Sí. [builtin] Type: Especifica cómo se maneja la lista de reproducción. Puede ser builtin (el programa la maneja), python o perl. Module: Nombre del módulo a pasarle al programa si la lista se maneja con Python o Perl. Poder configurar si la lista se maneja por defecto [builtin], por Python o Perl es lo que permite escribir funciones que manejen cuál es la siguiente canción a reproducir. En este caso, se usa Python. <Execution> <Background>0</Background> <Verbose>0</Verbose> <BaseDirectory>/tmp</BaseDirectory> </Execution> Background: El programa se ejecuta como demonio (despegado de consola); 0: No, 1: Sí. Verbose: El programa emite salidas de texto mas detalladas; 0: No, 1: Sí. BaseDirectory: Directorio base para crear archivos de comportamiento. <Stream> <Server> <Hostname>localhost</Hostname> <Port>8000</Port> <Password>letmein</Password> <Protocol>http</Protocol> </Server> ... </Stream> Hostname: Dirección o IP del servidor de streaming al cual conectarse. 33 Port: Puerto del streaming Password: Clave del usuario fuente (S.C.) en el servidor. Protocol: Encabezado del protocolo a usar, shoutcast: “icy”, icecast 1.x: “xaudiocast”, icecast 2: “http”. <Stream> <Server> ... </Server> <Mountpoint>/ices</Mountpoint> <Name>Default stream</Name> <Genre>Default genre</Genre> <Description>Default description</Description> <URL>http://localhost/</URL> <Public>0</Public> </Stream> Mountpoint: Punto de montaje en servidor Icecast. Name: Nombre del streaming. Genre: Estilo del streaming. Description: Descripción del streaming. URL: Dirección a una página externa. Public: Si el servidor puede publicar el streaming, 0: No, 1: Sí. 5.4.3. Ices y Python El sistema builtin de ices0 es suficiente para reproducir aleatoriamente canciones listadas en un archivo, pero el objetivo final que se busca en este trabajo es adaptar la lista de reproducción a los gustos de los usuarios. Por lo tanto, es necesario poder elegir la siguiente canción. El programa viene configurado para que, en caso de especificar python en vez de builtin, se llame a un módulo Python con cinco funciones básicas. El archivo que viene junto a los fuente de ices0, ices-0.4/conf/ices.py.dist, trae un ejemplo básico. Para mayor comodidad se crea el directorio /home/felipe/ices/conf/ para guardar los archivos Python y el XML de configuración. Las cinco funciones son: ices_init (): Se llama cuando arranca ices0 para iniciar el ambiente de Python. Retorna 1 no hay errores, o 0 si algo falla. 34 ices_shutdown (): Se llama al cerrar ices0 para detener el ambiente Python. Retorna 1 si no hay errores, o 0 si algo falla. ices_get_next (): Se llama para obtener la ruta de la siguiente canción a reproducir. Retorna un string. ices_get_metadata (): Retorna el string a usar como metadata. Si retorna null se usa el comentario del archivo. ices_get_lineno (): Se usa para escribir el número de la canción dentro del archivo /tmp/ices.cue donde se guardan datos de la canción en reproducción. Retorna un entero. Otro de los grandes problemas que tiene el método builtin, es que depende de una playlist externa creada de manera independiente, para generar su lista de reproducción interna. Por lo tanto cuando se agregan nuevas canciones a la radio, el orden se puede alterar y con ello los índices. Esto no sirve si se quiere emparejar las evaluaciones con dicho índice. Por lo tanto, en las funciones a escribir se desarrolla un sistema de revisión de canciones dentro de un directorio, que luego genera una lista externa en base a las canciones encontradas. Si después se agregan nuevas canciones al directorio, al volver a ejecutar ices0 se agregan al final de dicha lista, manteniendo el orden. Entonces, en la función ices_init() que inicia el ambiente Python, se lee el archivo /home/felipe/ices/playlist.txt y se ordenan las canciones en la lista playlist. Luego se revisa el directorio donde se encuentran todas las canciones a reproducir, (/mnt/mp3/Radio/), en busca de archivos MP3, y se busca cada uno de ellos en la lista playlist. Si se encuentra, pasa al siguiente. De lo contrario, se agrega al final de la lista y del archivo. De esta manera, cuando se vuelva a ejecutar ices0, si hay canciones nuevas, se agregan al final del archivo y se mantiene su numeración. Para poder recorrer un directorio es necesario importar el módulo os. 35 def ices_init (): global playlist global listaTop global prediccion global rutaonline fplaylist = open("/home/felipe/ices/playlist.txt", "a+") playlist = fplaylist.read() playlist = playlist.split("\n") for root,dirs,files in os.walk("/mnt/mp3/Radio"): for file in [f for f in files if f.lower().endswith(".mp3")]: if not(os.path.join(root, file) in playlist): playlist.append(os.path.join(root, file)) fplaylist.write(os.path.join(root, file) + "\n") fplaylist.close() ... Nótese que también se declaran como globales listaTop, prediccion y rutaonline. Luego se borra el contenido del archivo /var/www/.data/last20, que llevará el registro de las últimas 20 canciones reproducidas. Se definen las tres rutas necesarias: rutaonline: Ruta del archivo que contiene lista de usuarios conectados escuchando la radio (/var/www/.data/usersonline). rutausers: Ruta del directorio que contiene los archivos de usuarios (/var/www/.usuarios/). rutaPtoF: Ruta del archivo donde escribir predicciones (/var/www/.data/predicciones). Luego, se llama por primera vez a la función PredictTop (4.4.7). Para ello es necesario importar el modulo preditop, por lo que se guarda el archivo predictop.py junto con ices.py en /home/felipe/ices/conf/. El resultado del llamado a la función se guarda por separado en prediccion y listaTop respectivamente, y al declararlas globales se pueden usar en las demás funciones. 36 def ices_init (): ... flast20 = open("/var/www/.data/last20", ’w’) flast20.close() rutaonline = "/var/www/.data/usersonline" rutausers = "/var/www/.usuarios/" rutaPtoF = "/var/www/.data/predicciones" [prediccion, listaTop] = predictop.PredictTop(rutaonline, rutausers, 3, rutaPtoF) return 1 Con esto ya se obtiene una primera predicción y una lista de canciones populares, aunque es probable que como se está iniciando el sistema, nadie esté escuchando la radio, por lo tanto no exista la lista y sólo se almacene False. Lo siguiente que ocurre es el llamado a la función ices_get_next() para reproducir la primera canción. Esta contiene muchas variables globales, de la función anterior se obtiene playlist, prediccion, listaTop y rutaonline. songnumber guarda el número o índice de la canción a reproducir, ruta guarda en una cadena de texto la ruta de la canción a reproducir, times lleva la cuenta de cuántas canciones se han reproducido, y last20 es una lista de las últimas 20 canciones reproducidas. def ices_get_next (): global playlist global prediccion global listaTop global rutaonline global songnumber global ruta global times global last20 ... Lo primero es revisar si hay usuarios conectados escuchando la radio y guardar el contenido del archivo en la ruta rutaonline en users. Después se lee el contenido del archivo refresh en la variable refresh, el cuál cambia cuando un usuario evalúa una canción, indicando que es necesario actualizar las predicciones (ver 5.6.2 para más detalles). 37 def ices_get_next (): ... fonlineusers = open(rutaonline, ’r’) users = fonlineusers.read() fonlineusers.close() frefresh = open("/var/www/.data/refresh", ’r’) refresh = frefresh.read() frefresh.close() ... Entonces, si refresh contiene algo, se deben actualizar las predicciones. Por comodidad se vuelven a definir rutausers y rutaPtoF. Luego, se ejecuta la función PredictTop del módulo predictop para actualizar prediccion y listaTop, y se borra el contenido del archivo refresh para indicar que las predicciones están actualizadas. def ices_get_next (): ... if (len(refresh) != 0): rutausers = "/var/www/.usuarios/" rutaPtoF = "/var/www/.data/predicciones" [prediccion, listaTop] = predictop.PredictTop(rutaonline, rutausers, 3, rutaPtoF) frefresh = open("/var/www/.data/refresh", ’w’) frefresh.close() ... Si refresh no tiene contenido, significa que nadie ha evaluado alguna canción (o cambiado alguna evaluación), por lo que no es necesario actualizar las predicciones. Entonces, se revisa el contenido de users (si hay usuarios escuchando la radio). De ser así, es necesario adaptar el listado de canciones a reproducir para que se ajuste a los gustos de los oyentes. Como esto no necesita actualizar las predicciones, se puede llamar por separado a la función TopTracks del módulo predictop, y cambiar solamente el contenido de listaTop. De no haber usuarios escuchando la radio, entonces listaTop valdrá False. 38 def ices_get_next (): ... elif (len(users) != 0): users = users.split("\n") users.pop() listaTop = predictop.TopTracks(prediccion, users, 3) else: listaTop = False ... Después de revisar si hay usuarios escuchando la radio, se borra el contenido del archivo indicado en rutaonline. Luego, dependiendo si hay listaTop o no, nouserrand toma un valor de 4 si hay usuarios conectados o de 1 si no los hay. Luego se entra a un ciclo que cumple dos tareas. La primera, dependiendo del valor actual de times y nouserrand, decide si el número o índice de la siguiente canción se elige aleatoriamente de la listaTop o la playlist. Si nouserrand vale 1, significa que no hay usuarios escuchando y el módulo con times siempre será cero, por lo que songnumber saldrá de la playlist. Si nouserrand es 4 significa que hay alguien escuchando, entonces 3 de cada 4 veces songnumber saldrá de listaTop y el resto de playlist, por lo tanto una de cada 4 canciones es completamente aleatoria. Esto sirve para agregarle novedad a la lista de reproducción. La segunda tarea es mantener un listado en last20 de las últimas 20 canciones reproducidas. Esto se logra revisando el nuevo songnumber; si está dentro de la lista, se vuelve a ejecutar el ciclo. De lo contrario, se agrega a la lista, se termina el ciclo, se busca la ruta de la canción a reproducir y se retorna como cadena de texto. 39 def ices_get_next (): ... if listaTop: nousersrand = 4 else: nousersrand = 1 while True: if (times%nousersrand == 0): songnumber = random.randrange(0, len(playlist)) else: songnumber = random.randrange(0, len(listaTop)) songnumber = int(listaTop[songnumber]) times += 1 if (len(last20)>20): last20.pop(0) if songnumber in last20: continue else: last20.append(songnumber) break ruta = playlist[songnumber] return ruta Como las evaluaciones siempre fueron guardadas con el índice respecto a playlist, no hay problema de obtener la ruta desde ella. La siguiente función en ser llamada es ices_get_metadata(), la que se encarga de enviar la cadena de texto a usar como metadata de la canción en reproducción, o null para usar el comentario del archivo. Además se aprovecha para ir actualizando el archivo /var/www/.data/last20 con las 20 últimas canciones reproducidas. Para esto se define como global ruta para obtener la ruta del archivo MP3, y songnumber para agregar el índice al archivo de las últimas 20. Luego, se abre el archivo MP3 como binario con permiso de lectura y se leen los últimos 128 bytes de información, que contienen la información según ID3 (v1) [10]. Si los primeros tres bytes corresponden a la sigla TAG, entonces 40 corresponde a la metadata del archivo. En un diccionario trackdata se guarda el título, el artista y el album de la canción, y en infotrack se arma la cadena de texto que retorna la función. def ices_get_metadata (): global ruta global songnumber mp3 = open(ruta, "rb") mp3.seek(-128,2) metadata = mp3.read(128) mp3.close() infotrack = null if(metadata[:3] == "TAG"): trackdata = {"title": stripnulls(metadata[3:33]), "artist": stripnulls(metadata[33:63]), "album": stripnulls(metadata[63:93])} infotrack = trackdata["artist"] + " - " + trackdata["title"] En local20 se lee y guarda el contenido del archivo /var/www/.data/last20, como una lista de cadenas de texto separadas por un salto de línea, y en caso de tener más de 20 canciones listadas, se elimina la última. Luego se borra el contenido del archivo y, por cada elemento en local20, si el índice de la canción no es songnumber (la canción actual), se escribe la misma cadena de texto. De lo contrario se escribe el índice y la información de la canción según el formato: índice:artista - título Esto mantiene cada nuevo índice de la canción en reproducción junto a la información del archivo. Finalmente se retorna la cadena de texto a servir como metadata, o null si no se pudo leer el ID3 del archivo. 41 def ices_get_metadata (): ... local20 = [] flast20 = open("/var/www/.data/last20", ’r’) local20 = flast20.read() flast20.close() local20 = local20.split("\n") local20.pop() if (len(local20) == 20): local20.pop(0) flast20 = open("/var/www/.data/last20", "w") for track in local20: writen = track.split(’:’) if (writen[0] != str(songnumber)): flast20.write(track + "\n") flast20.write(str(songnumber) + ’:’ + infotrack + "\n") flast20.close() return infotrack Finalmente, se llama a la función ices_get_lineno() para escribir el índice de la canción actual en el archivo /tmp/ices.cue. Entonces, basta con reconocer songnumber como variable global y retornar su valor. def ices_get_lineno (): global songnumber return songnumber Esto concluye la implementación de la predicción y adaptabilidad de la lista de reproducción usando el módulo escrito para ices0. Ambos archivos Python (predictop.py y ices.py) se guardan en el directorio /home/felipe/ices/conf/, junto con el archivo XML de configuración icespy.xml. Para ejecutar el programa, se debe ir por consola al directorio donde se encuentra el archivo XML de configuración de ices0. Luego se llama a ices desde línea de comando, junto con la opción -c para especificar el archivo de configuración, y -B si se desea una ejecución separada de consola. En este caso ese directorio es home/felipe/ices/conf/. 42 cd /home/felipe/ices/conf/ ices -c icespy.xml -B Una vez ejecutado, ices0 genera tres archivos en el directorio /tmp/ ices.cue: Contiene información sobre la canción en reproducción: nombre del archivo, tamaño (en bytes), bitrate (en kbits/s), minutos:segundos (totales), porcentaje de reproducción, índice en la playlist, ID3 artista, ID3 título. ices.log: Registro de cada inicio, mensajes, canciones reproducidas y errores de ices0. ices.pid: ID del proceso en el sistema operativo de ices0. 5.5. PHP y Wimpy Button 5.5.1. PHP básico Para instalar PHP se puede usar el gestor de paquetes. sudo apt-get install php5 Luego, se escribe una sencilla página web en /var/www/index.php, la que se muestra a continuación. <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> </head> <body> <h1>RaDIE</h1> <p>Proyecto en proceso, disfrute por mientras</p> <!-- Insertar Web Player Aquí --> </body> Ahora sólo falta agregar el reproductor web embebido. 43 5.5.2. Wimpy Button Para agregar el reproductor embebido Wimpy Button, se descargan los archivos y se procede a su configuración. Para reproducir el streaming, es necesario que la URL del stream termine en “.mp3”, de lo contrario no reconocerá el tipo de audio. Por lo tanto, se configura dicha url en 5.4.2 para que sea como se muestra a continuación. http://hopper.die.udec.cl:8000/RaDIE.mp3 En los parámetros del reproductor se debe agregar lo siguiente: bufferaudio=10: Tamaño del buffer [5-10]. icecast=300: Segundos que Wimpy debe reproducir antes de volver a conectarse al stream. El segundo parámetro indica renovar la conexión. Esto es así ya que los reproductores flash almacenan los datos del stream en memoria, y si no se establece una nueva conexión, el tamaño de esa cola crece demasiado y empieza a fallar. Una nueva conexión cada cierto tiempo arregla ese problema. Aún cuando los parámetros se pueden agregar manualmente, es recomendable hacerlo mediante el Wimpy Button Maker, disponible gratuitamente en la misma página de Wimpy Button, ya que de otra manera no se garantiza un correcto funcionamiento. Agregando el reproductor configurado para recibir el stream desde Icecast, la página queda como se muestra en la figura 5. Esta radio online permite a quien ingrese escuchar las canciones transmitidas. Figura 5: Página sólo con reproductor embebido. 5.6. Usuarios y evaluaciones Como la radio ya cuenta con una página con reproductor embedido en funcionamiento, el siguiente paso es poder crear cuentas de usuarios que se conecten a evaluar las canciones. 5.6.1. Creando cuentas de usuarios Para realizar esto se crea el directorio /var/www/.usuarios/, donde cada usuario tendrá un archivo en el cual se guardará su información. En la primera línea se almacena la codificación en MD5 44 de la clave del usuario, en la segunda línea el correo electrónico y de la tercera en adelante se guarda el par “número de canción” y “evaluación”. Las evaluaciones son enteros del 1 al 5. Entonces, el usuario al visitar la página por primera vez, deberá crear una cuenta para evaluar las canciones. La página de inicio se muestra en la figura 6a, y cuando ya ingresó como usuario válido se muestra en la figura 6b. (a) (b) Figura 6: Página de inicio (a). Sin cuenta de usuario. (b). Con cuenta de usuario. Para crear una cuenta de usuario, la página lleva a newuser.html. En la figura 7a se muestra la página sin datos, y en la figura 7b con datos ingresados en los espacios pertinentes. (a) (b) Figura 7: Creando una nueva cuenta (a). Sin datos. (b). Con datos. En caso de que falte algún dato en el formulario al crear una cuenta, se despliegan distintos mensajes de error acordes. En la figura 8a faltó indicar el nombre de usuario, en la figura 8b faltó ingresar la clave, y en la figura 8c faltó ingresar el correo electrónico. En último caso que haya un error al ingresar los datos para crear una cuenta, también se deben desplegar mensajes de error acordes. La figura 9a muestra el mensaje en caso de ya existir la cuenta a crear, y la figura 9b el caso en que las contraseñas no coincidan. 45 (a) (b) (c) Figura 8: Faltan datos para crear una cuenta (a). Sin nombre de usuario. (b). Sin clave. (c). Sin correo. (a) (b) Figura 9: Errores creando una cuenta (a). Usuario ya existe. (b). Claves no coinciden. Por otra parte, si el error ocurre al tratar de ingresar con la cuenta de usuario, también es necesario desplegar mensajes de error acordes. La figura 10a muestra el error cuando se trata de ingresar con una cuenta que no existe, y la figura 10b el error cuando la contraseña es incorrecta. (a) (b) Figura 10: Errores al ingresar (a). Usuario no existe. (b). Clave incorrecta. Con esto se pueden crear cuentas de usuarios exitosamente. Para reconocer al usuario cuando entre a la radio se crea una COOKIE llamada uRaDIE, de tal manera que no esté obligado a escribir todas las veces su nombre de usuario y contraseña, más bien su navegador lo recuerda por él. Lo siguiente es habilitar el sistema para que puedan evaluar las canciones en reproducción. 46 5.6.2. Sistema de evaluación Una vez conectado el usuario a la radio online, debe poder evaluar la canción en reproducción. Para esto se agrega un pequeño formulario con cinco botones de opción, donde cada uno tiene el valor las posibles notas, del 1 al 5 ordenadamente. Además, se agrega un botón para enviar la evaluación, como se muestra en la Figura 11. Este botón puede resolver el formulario hacia otra página PHP o hacia alguna función en Javascript. En este caso la segunda opción es preferible, ya que permite utilizar AJAX para enviar de manera asíncrona la información y así evitar la necesidad de refrescar la página para evaluar otra canción. Figura 11: Botones de opción y enviar. Esto se incluye en la página principal, debajo del reproductor embebido. La función Javascript donde resuelve el botón enviar es sendEval(ID, form, rbname) incluida en el archivo funciones.js, y sus argumentos son tres. ID: ID del bloque HTML que recibirá el resultado del llamado a la función. form: ID del formulario que contiene los botones de opción. rbname: Nombre del conjunto de botones de opción. Esta función se encarga de procesar la información enviada en los botones de opción para obtener la evaluación y luego enviarla a eval.php mediante el método GET de PHP. Una vez procesada por dicha página, retorna la cadena de texto “Su evaluación fue: X”, donde X es la evaluación ingresada, y esa cadena es enviada como resultado al bloque HTML indicado. El trabajo que realiza eval.php es abrir el archivo del usuario y reescribirlo, pero actualizando o añadiendo la evaluación ingresada. Además, escribe un 1 en /var/www/.data/refresh para avisar que es necesario actualizar las predicciones. De esta manera se puede evaluar la canción en reproducción. También es necesario mostrar si ya fue evaluada antes. Para esto se agregan dos líneas bajo el formulario, una indicando la cuenta de usuario que se está usando para evaluar canciones, y la segunda para saber el estado de la evaluación. Este estado debe actualizarse cuando el usuario evalúe o la canción cambie. Para ello se escribe una función usando jQuery que refresca el mensaje cada cinco segundos, el cual se obtiene llamando a geteval.php. Esta página revisa si existe la evaluación de una canción determinada, y retorna una cadena de texto acorde. Los posibles mensajes son los siguientes, siendo X la nota ingresada: “No has evaluado esta canción.”: Cuando la canción no ha sido evaluada. 47 “Su evaluación fue: X”: Cuando recién se evalúa una canción. “Evaluaste esta canción con un X”: Cuando la canción fue evaluada. De la misma manera, se escriben dos funciones más usando jQuery: para refrescar el artista y título, y el tiempo reproducido y total de la canción. El primer resultado se ubica entre el formulario y el reproductor Wimpy, y se actualiza cada cinco segundos llamando a getTrackInfo.php. El segundo resultado se ubica al lado derecho del reproductor, y se actualiza cada un segundo llamando a getTime.php. En ambos casos la información se obtiene del archivo ices.cue descrito en la sección 5.4.3. Dicho archivo no contiene siempre la duración total de la canción, por lo tanto, en esos casos no se pueden calcular los tiempos de reproducción. A petición de algunos usuarios, se desarrolla una página (last20.php) donde se pueden ver las últimas veinte canciones reproducidas. En ella se puede evaluar alguna canción recién reproducida pero que no fue evaluada en su momento, y además sirve para aumentar la cantidad de evaluaciones, ya que al reconocer alguna canción, se evalúa sin tener que esperar su reproducción. Esta página debe ser refrescada por el usuario. El resultado de todo lo agregado en esta sección se muestra en la figura 12a. Además, para poder llevar un registro de cómo predice el sistema, cada vez que alguien evalúa una canción predicha, ya sea en el momento que es reproducida o mientras pertenece a las últimas 20, en el archivo /var/www/.data/PvsE se anota el nombre del usuario evaluador, la evaluación predicha o anterior (dependiendo si se había evaluado antes o no), la nueva evaluación y las siglas PE si se evaluó una canción predicha o EE si se cambió la evaluación ingresada. Las canciones que no han sido evaluadas, y por lo tanto no pueden tener predicción, se ingresan como PE pero con nota 0. 5.6.3. Detectando oyentes Lo siguiente es desarrollar la detección de usuarios conectados escuchando la radio. Esto es muy necesario, ya que no basta con que un usuario ingrese con su cuenta, esta puede quedar abierta indefinidamente, lo que no significa que esté oyendo la radio. Para esto se hace uso de AJAX y HTML DOM [11], manejando los eventos que se generan al abrir y cerrar una página web, onload y onunload respectivamente. Ambos pueden llamar a funciones Javascript para ejecutar ciertas acciones cuando ocurran. Entonces, el evento onload llama a la función openpage(), donde se envía mediante GET el nombre de usuario ejecutor a listening.php, para actualizar el registro de los usuarios que están escuchando la radio. De igual manera, el evento onunload llama a la función closepage(), donde se envía mediante GET el nombre de usuario ejecutor a notlistening.php, para actualizar quién dejó de escuchar la radio. 48 (a) (b) Figura 12: Páginas de inicio y últimas 20, (a). Una canción evaluada. (b). Últimas 4 de las 20. Por desgracia, el evento onunload tiene un comportamiento irregular, asumible a que hay muchas maneras de cerrar una página. Además el evento onload es insuficiente para detectar oyentes, ya que pueden abrirla múltiples veces. Esto obliga a añadir maneras adicionales para detectar usuarios conectados oyendo la radio. En la sección 5.6.2, se desarrollan funciones que se conectan de manera asíncrona al servidor para refrescar pequeñas partes de la página. Por lo tanto, se decide darle la tarea a una de ellas, getTrackInfo.php, de actualizar el listado, incluyendo el nombre del usuario que está solicitando refrescar el nombre del artista y título de la canción. Por esto, se modifica para que obtenga esos datos mediante una SESSION de PHP, los cuales serán obtenidos en getTime.php, la solicitud que se genera cada un segundo. Y para saber cuando dejan de escuchar la radio, cada vez que se cambie la canción, se borra el contenido de usersonline. De esta manera, si el usuario se mantiene escuchando la radio, vuelve a ser escrito en el archivo. 5.7. Resumen de directorios y archivos Para una mayor claridad, a continuación se escribe un resumen de todos los directorios y archivos que se crean y/o usan para el correcto funcionamiento de esta radio online. 5.7.1. Directorios /etc/icecast2/: Donde se guarda el archivo XML de configuración para Icecast. 49 /home/felipe/ices/: Donde se ubica el directorio conf/ y la playlist externa que maneja el módulo ices.py. /home/felipe/ices/conf/: Donde se guardan y manejan los archivos Python ices.py y predictop.py, junto con el archivo XML de configuración de ices0 icespy.xml. /var/www/: Donde se guardan todos los archivos que se usan para armar la página web de la radio online. Además contiene los directorios .usuarios/ y .data/. /var/www/.usuarios/: Donde se crean y manejan todos los archivos de usuarios que contienen sus evaluaciones. Cada archivo es un usuario. /var/www/.data/: Donde se guardan los archivos de funcionamiento o registro de la radio online. /mnt/mp3/Radio/: Donde se guardan todos los archivos MP3 a reproducir en la radio. 5.7.2. Archivos En /etc/icecast2/: icecast.xml: Archivo XML de configuración para Icecast. En /home/felipe/ices/: playlist.txt: Archivo de texto que sirve de playlist. Lo genera y maneja ices0 cuando funciona con Python. En /home/felipe/ices/conf/: predictop.py: Contiene el algoritmo de predicción y las funciones para generar la lista adaptativa. ices.py: Contiene las funciones que llama ices0 cuando se configura para funcionar mediante Python. Hace uso de predictop.py. icespy.xml: Archivo de configuración de ices0. En /var/www/: index.html: Página principal de la radio online. Contiene el reproductor web con tiempo e información de canción actual, link a FAQ.html, y si no se ha ingresado con cuenta, link a newuser.html. Si se ingresó, muestra el formulario para evaluar, nombre de usuario, estado de evaluación, predicción, link a last20.php y link a singout.php. 50 newuser.html: Página que contiene el formulario para crear un nuevo usuario. Para confirmar datos, llama a nuevousuario.php . FAQ.html: Página con preguntas frecuentes. ingresar.php: Página para ingresar como usuario a la radio. Revisa datos de ingreso y crea la COOKIE uRaDIE. singout.php: Página para desconectarse como usuario, borra la COOKIE uRaDIE eval.php: Ingresa la evaluación de la canción en reproducción, registra el cambio en PvsE y cambia contenido de refresh. evalfixed.php: Ingresa la evaluación de la canción indicada mediante GET. Similar a eval.php. getEval.php: Llamada por la función jQuery para refrescar el estado de la evaluación. Si no a sido evaluada, carga posible predicción. getTime.php: Llamada por la función jQuery para refrescar los tiempos de duración y reproducción de la canción. Obtiene información del archivo /tmp/ices.cue, y guarda en SESSION la información para getTrackInfo.php. Retorna ambos tiempos en una cadena de texto. getTrackInfo.php: Llamada por la función jQuery para refrescar artista y título de la canción. Además registra en usersonline el nombre de usuario conectado escuchando la radio. last20.php: Página que carga las últimas 20 canciones reproducidas con sus respectivos formularios para evaluarlas, usando la función Javascript sendEvalFixed. listening.php: Llamada por la función Javascript openpage para registrar el ingreso de un usuario a la página. notlistening.php: Llamada por la función Javascript closepage para registrar el cierre de un usuario a la página. nuevousuario.php: Revisa todos los datos ingresados para crear un nuevo usuario. Si no hay problemas lo crea, de lo contrario avisa los errores. funciones.js: Contiene las funciones Javascript getCookie, openpage, closepage, sendEval, sendEvalFixed y las tres funciones jQuery que permiten refrescar automáticamente los tiempos de la canción cada 1 segundo, y la información y estado de evaluación cada 5. jquery-1.2.6.min.js: Archivo Javascript que habilita el uso de jQuery. jquery.timers-1.0.0.js: Plugin de jQuery para usar eventos temporales. 51 En /var/www/.data/: last20: Registro de últimas 20 canciones reproducidas. PvsE: Registro de cambio de evaluaciones o evaluar canciones predichas. refresh: Registro que indica una nueva evaluación, o actualización de alguna ya realizada. usersonline: Registro de los usuarios conectados escuchando la radio. predicciones: Contiene todas las predicciones de todos los usuarios evaluadores. En /tmp/: ices.cue: Contiene información sobre la canción en reproducción: nombre del archivo, tamaño (en bytes), bitrate (en kbits/s), minutos:segundos (totales), porcentaje de reproducción, índice en la playlist, ID3 artista, ID3 título. ices.pid: ID del proceso en el sistema operativo de ices0. ices.log: Registro de cada inicio, mensajes, canciones reproducidas y errores de ices0. 52 Capítulo 6. Pruebas y resultados 6.1. Introducción En esta sección se realizan pruebas al algoritmo desarrollado en la sección 4 y se presentan los resultados obtenidos de la radio online por parte de distintos usuarios. 6.2. Pruebas al algoritmo Lo primero es estudiar si a baja escala las predicciones son correctas. Para esto se preparan pruebas con matrices de distintos tamaños. 6.2.1. Matriz de 2x3 La primera prueba es con una matriz X de 2x3, 2 canciones vs. 3 usuarios, donde el primer usuario no evaluó la canción 1, y evaluó la canción 2 con un 5. El segundo usuario evaluó ambas con un 5, y el tercero ambas con un 1. Tanto el método de Flycasting [2] como el propuesto predicen el mismo resultado. Esto ya que Flycasting sólo utilizará la información otorgada por el segundo usuario, por ser similar al primero. Con el método propuesto el tercer usuario queda clasificado como disímil, por lo tanto la predicción según la ecuación (6) también sería un 5, y ponderando ambas se obtiene el mismo resultado. 0 5 X = 5 5 1 1 5 5 X 0 = 5 5 1 1 Esta prueba, por simple que parezca, muerta que la solución propuesta no entorpece las predicciones a un nivel básico. 6.2.2. Matriz de 4x5 La siguiente prueba se realiza con una matriz Z de 4x5, 4 canciones y 5 usuarios. El usuario 1 es similar al 2, disímil al 3 y difiere opuestamente en una evaluación con el usuario 4. El usuario 2 tiene dos evaluaciones idénticas con el usuario 4 pero una muy distinta. El usuario 3 es disímil a los usuarios 1 y 2, y a medias con el usuario 4. Además se introduce un usuario que no evaluó nada, su predicción será la misma como si hubiera evaluado una canción que nadie más evaluó. Las predicciones se realizan con un umbral de pesos de 0,7. Z 0f ly es el resultado del algoritmo Flycasting, y Z 0 el del propuesto. Lo primero que se observa es que Z 0f ly tiene menos predicciones, resultado esperado al descartar los usuarios disímiles, y por lo 53 mismo no puede reconocer que el usuario 3 hasta el momento es completamente contrario al usuario 1, y no puede obtener predicción para la canción 2. Z 0 por su parte si lo reconoce y predice sin problemas para la canción 2, con un 5 para el usuario 2 y un 1 para los usuarios 3 y 4. El usuario 4 no es tan contrario al 1, pero debido a la drástica clasificación entre similares y disímiles, puede que la predicción no sea del todo indicada. Z = 1 1 5 1 0 5 0 0 0 0 1 1 5 5 0 0 2 0 2 0 Z 0f ly = 1 1 5 1 0 5 5 0 0 0 1 1 5 5 0 2 2 0 2 0 Z 0 = 1 1 5 1 0 5 5 1 1 0 1 1 5 5 0 3 2 4 2 0 1 1 0,36 0,68 0 1 1 0,36 0,79 0 Pesos = 0,36 0,36 1 0,68 0 0,68 0,79 0,68 1 0 0 0 0 0 1 Otra diferencia interesante de la propuesta se aprecia en la canción 4, donde pese a que sólo dos usuarios la han evaluado y con notas bajas, la predicción para el usuario 1 fue un 3 y para el usuario 3 fue un 4. En el caso del usuario 1 se debe a que ambos usuarios que evaluaron quedan clasificados en grupos contrarios, por tanto uno predice un 2 y el otro un 4, ponderando resulta un 3. Para el usuario 3 ambos usuarios quedan clasificados como disímiles, por tanto se le predice un 4. Y finalmente el usuario 5 no tiene ninguna evaluación en común, por lo tanto, no se puede predecir sus evaluaciones. Debido a esto, es recomendable omitir a los usuarios que no han evaluado canciones del algoritmo de predicción, ya que ahorra tiempo de cálculo. El análisis del algoritmo con matrices de orden superior resulta muy complejo, por lo que es preferible observar resultados concretos dentro del sistema completo. 6.3. Tiempos del algoritmo Cuando se cambia de una canción a otra, se puede realizar la predicción y adaptar la lista en ese momento, siempre cuando el tiempo de cálculo que requiere sea muy bajo, de ordenes menores a un segundo. Hay dos factores que afectan el rendimiento del algoritmo: Cantidad de usuarios y cantidad de evaluaciones. 54 Figura 13: Tiempos del algoritmo. Cantidad de usuarios: Mientras más usuarios, mayor es la cantidad de predicciones que se deben realizar. Cantidad de evaluaciones: Mientras más evaluaciones, mayor la cantidad de evaluaciones que se debe predecir para cada usuario. De la sección 4.4 se observa que el algoritmo predictivo es de orden O(n2 ), lo que significa que a mayor número de usuarios y evaluaciones, el tiempo de cálculo aumenta considerablemente. En la Figura 13 se puede apreciar el rendimiento promedio del algoritmo. Mientras más usuarios y más evaluaciones tengan, más demora en predecir y lograr adaptar la lista de canciones con nota sobre 3, suponiendo que están conectados escuchando al mismo tiempo. Con 300 usuarios y 200 evaluaciones cada uno, demora al rededor de un minuto y medio. Esto muestra que a un alto uso de la radio online es necesario optimizar el rendimiento del algoritmo, como también la manera de usarlo. Para ambos casos, una buena opción es paralelizar los procesos, ya que el cálculo de pesos y predicciones es altamente paralelizable, y mientras suena una canción se alcanzaría a calcular las predicciones y lista para la siguiente reproducción. Otra opción para mejorar el rendimiento es trabajar con lenguajes de nivel más bajo, como C o C++. Esto permitiría optimizar el uso de memoria y tiempos de ejecución. 55 Figura 14: Cuántas evaluaciones tiene cada canción. 6.4. La radio online La radio fue implementada con 16128 canciones, una alta cantidad y variedad de estilos musicales. En cuanto a personas, se llegó a 39 usuarios con alguna evaluación, de 51 usuarios en total. Esto quiere decir que 12 crearon cuentas y nunca evaluaron alguna canción. Ellos no afectan el rendimiento del sistema, ya que a la hora de predecir son ignorados. En la tabla 3 se muestran los usuarios clasificados en cuatro grupos, según cuántas evaluaciones realizaron. Se puede ver que casi la mitad sólo evaluaron una canción, un tercio menos de 10, un 15.4 % menos de 100 y tan solo 3 usuarios sobre 100 evaluaciones. Por lo tanto, se puede apreciar de la figura 13, que el tiempo necesario para predecir y generar la lista es muy menor a un segundo. Tabla 3: Cantidad de usuarios y evaluaciones No Evaluaciones No Usuarios 1 17 < 10 13 < 100 6 ≥ 100 3 Total 39 % 43.6 33.3 15.4 7.7 100 En la figura 14 y la tabla 4 se muestra la cantidad de evaluaciones que tiene cada canción. Al tener una gran variedad de archivos, no sorprende el elevado porcentaje de evaluaciones que no han sido evaluadas, sólo cerca del 4 % tiene evaluaciones. Además, de ese grupo, más de la mitad solo 56 Tabla 4: Cantidad de evaluaciones por canción No Evaluaciones No Canciones % 0 15512 96.18 1 363 2.25 2 183 1.13 3 50 0.31 4 17 0.11 5 2 0.01 6 1 0.01 Total 16128 100 Figura 15: Evaluaciones de 6 usuarios. posee una evaluación, lo que quiere decir que la predicción se realiza con muy poca información, y clasifica a los usuarios entre similares y disímiles basándose en unas pocas canciones evaluadas en común. Para estudiar el efecto de lo anterior, se requiere observar las evaluaciones de algunos usuarios, y qué tal predijo el sistema para ellos. En la figura 15 y la tabla 5 se muestran las evaluaciones de seis usuarios distintos. El nombre de cada usuario fue modificado para mantener la privacidad de sus evaluaciones. El usuario 1 evaluó pocas canciones, apenas 22. El usuario 2 evaluó 100 canciones y la mayoría negativas. El usuario 3 evaluó 81 canciones y la mayoría positivas. El usuario 4 evaluó 376 canciones, es el con más evaluaciones y su tendencia es media. El usuario 5 evaluó 58 canciones con tendencia 57 Tabla 5: Evaluaciones de los 6 usuarios Usuario No Evaluaciones 1 22 2 100 3 81 4 376 5 58 6 108 1 2 3 1 7 6 45 15 17 2 3 9 32 63 168 25 8 15 18 22 17 4 4 14 22 88 6 29 5 4 9 45 25 4 22 Tabla 6: Cantidad de canciones evaluadas en común Usuario 1 1 X 2 5 3 3 4 11 5 0 6 4 2 3 4 5 3 11 X 10 36 10 X 48 36 48 X 15 5 24 10 10 64 5 6 0 4 15 10 5 10 24 64 X 6 6 X negativa. El usuario 6 evaluó 108 canciones y sus notas son muy variadas. Como se puede apreciar, aún con la baja participación dentro del periodo de prueba del sistema, se logra armar un grupo de usuarios con comportamiento variado. La cantidad de canciones evaluadas en común entre ellos se muestra en la tabla 6. Como el usuario 4 ingresó más evaluaciones, es esperable que sea el que tiene más en común con los demás, y además sea el que más influencia las predicciones para el resto. Si se omite ese usuario, la mayor coincidencia es entre el 2 y el 5 con 15 canciones en común. Un dato importante es que los usuarios 4 y 6 son los más antiguos dentro del sistema. De hecho, comenzaron a evaluar canciones cuando la radio no predecía aún, y la mayoría de sus evaluaciones fueron ingresadas en ese periodo, definiendo la base inicial de temas a predecir y reproducir para los demás usuarios. La comparación entre predicciones y evaluaciones se muestra en la figura 16. Por ser los más antiguos, la evolución de los usuarios 4 y 6 está incompleta, ya que no se llevaba registro de predicciones inexistentes al inicio del funcionamiento de la radio online. De todas formas, se puede apreciar cierta similitud o tendencia entre la predicción y su evaluación para ellos, lo que no es sorpresa si son los generadores de la base inicial de temas. Los demás usuarios sí tienen su historial de evaluaciones completo. Algunas predicciones son bajo 3, el umbral fijado para generar la lista, y por tanto no deberían sonar mientras se conecten esos usuarios. Si están presentes se debe a dos posibilidades: 58 Figura 16: Predicciones vs. evaluaciones. La canción con predicción bajo el umbral de reproducción estaba sonando mientras ingresó el usuario a la radio. El usuario evaluó la canción estando dentro de la lista de las últimas 20. Lo importante es que la mayoría de las canciones predichas que evaluaron los usuarios tenían una predicción igual o superior a 3. Para el usuario 1, 14 de las 22 evaluaciones realizadas fueron hechas contra predicciones. Inicialmente hay grandes diferencias entre ambas, y a medida que más evalúa pareciera que empiezan a tener la misma tendencia. El usuario 2 tiene casi un 50 % de sus evaluaciones realizadas contra predicciones. De la figura 15 se recuerda que tiene una tendencia muy negativa, comportamiento reflejado nuevamente. Al inicio se da el caso grave de predecir un 5 y recibir un 1, pero errores de este tipo son esperados en nuevos usuarios. Además, este usuario reveló que la mayoría de sus evaluaciones fueron realizadas directamente en el listado de las últimas 20 canciones, y que no se quedaba mucho tiempo escuchando la radio. Por lo tanto, mucha de la información es ingresa al mismo tiempo, lo que no le permite al sistema adaptarse al usuario paulatinamente, sino que trata de predecir abruptamente un nuevo resultado. Similar es el usuario 5, donde las predicciones no logran ser cercanas a sus evaluaciones. Estos casos son ejemplos de que el algoritmo es muy drástico en clasificar usuarios o similares o disímiles. El caso del usuario 3 es todo lo contrario. Un poco más del 50 % de sus evaluaciones fueron hechas contra predicciones, y no existen canciones predichas que hayan sido evaluadas con un 1, apenas dos canciones con predicción 3 fueron corregidas a un 2. Este usuario se conectaba muy seguido y 59 por largos tiempos a la radio, lo que permite ir ajustando los resultados de manera regular. Se debe tener en cuenta que la tendencia de este usuario es muy positiva, por tanto, los buenos resultados pueden no deberse necesariamente al algoritmo. Valiosa es la información otorgada por los usuarios más antiguos, 4 y 6. Opinan que cuando no existía predicción alguna, se reproducían muchas canciones que no eran de sus gustos, pero desde que iniciaron las predicciones, la lista mejoró notablemente. En otras palabras, la radio comenzó a adaptar el listado de acuerdo a la información entregada por ellos hasta entonces. Si bien la radio adapta el listado a los gustos ingresados por los usuarios, con la información recopilada se aprecia que el algoritmo no es rápido, necesita de una alta cantidad de evaluaciones y puede que sea insuficiente. 60 Capítulo 7. Conclusiones 7.1. Resumen Se logró implementar una radio online. Se puede acceder a ella desde cualquier locación y sin necesidad de instalar programas adicionales. Se desarrolló un sistema de cuentas de usuarios. Cualquier persona puede crear una cuenta, eligiendo nombre y contraseña. También se detecta a los usuarios conectados escuchando la radio. Se desarrolló la propuesta del algoritmo predictivo de manera exitosa. Se implementó un sistema de evaluación a las canciones por parte de los usuarios. Se pueden evaluar la canción en reproducción y las últimas 20 de la lista. Se puede ver y cambiar la nota. Se implementó el algoritmo en la radio online. Se puede conocer la nota predicha para las canciones del listado. Se logró generar una lista adaptativa. Cambia de acuerdo a los gustos de los oyentes. 7.2. Conclusiones Se puede implementar una biblioteca de música, en formato radio online, que transmita canciones vía streaming, seleccionadas de manera colaborativa por los gustos de los usuarios conectados escuchando en el momento. Para generar dicho streaming, se trabajó con los programas Icecast y Ices, ambos creados y mantenidos por la misma organización. Su configuración es simple, no requiere conocimientos avanzados en transmisión de datos para usarlos, son gratuitos, cuentan con buen soporte y son muy estables. Fue posible manipular el listado a reproducir, escribiendo módulos en lenguaje Python. En tanto, la reproducción del streaming en la página web se realizó con la versión demo de la aplicación flash Wimpy Button. De esta manera, las personas que ingresan no necesitan instalar ningún tipo de reproductor de audio, basta con que ya tengan la extensión de flash instalada en su navegador. Wimpy Button es sencillo y se puede configurar para trabajar con un stream proveniente de Icecast, renovando la conexión cada cierto tiempo para evitar mal uso de memoria. A falta de un reproductor web que extrajera la información de las canciones de la metadata directamente del stream, para mostrar el nombre de la canción, el artista, y los tiempos de duración y reproducción, se utilizó AJAX, creando solicitudes para buscar esos datos en el servidor y los refrescaran de manera asíncrona en la página. 61 La detección de oyentes en la radio, fue realizada mayormente gracias a dichas solicitudes AJAX, las cuales mantienen un registro de todos los que las emiten. También se detectan los eventos de abrir y cerrar la página, para ingresar o borrar al usuario de dicho registro. Usando filtrado colaborativo, fue posible clasificar a los usuarios en los dos grupos propuestos: similares y disímiles. Para esto se compararon las canciones evaluadas que tienen todos los usuarios en común, reflejando su grado de similitud en pesos calculados usando MDS y el algoritmo de correlación de Pearson, basado en el método desarrollado en Flycasting [2]. Sobre el algoritmo propuesto, se logra el objetivo planteado, predecir evaluaciones según los gustos de otros usuarios, aunque la información presentada muestra que, a corto plazo, es insuficiente como método único, es necesario complementarlo o mejorarlo de alguna manera. Se necesita más tiempo para estudiar el comportamiento a largo plazo. Para elegir la siguiente canción, primero se obtiene el listado de oyentes en línea. Luego, se buscan todas las canciones evaluadas y predichas de cada usuario, y se crea otra lista con las que tengan como mínimo una nota promedio media (3). Finalmente, se elije al azar cualquier canción de ese conjunto, teniendo en claro cuáles han sido las últimas 20, para evitar repetirlas en el corto plazo. Para agregar cierta novedad, cada 3 canciones se elige una canción aleatoria de todas las disponibles. Ciertamente, con un orden cuadrático O(n2 ), los tiempos de cálculo serán altos frente a más de 50 usuarios con 100 evaluaciones cada uno, por lo que se requiere buscar mejoras de rendimiento, como cambiar el lenguaje de programación a uno que permita optimizar recursos (C, C++), métodos de reducción de orden para matrices, explotar el potencial paralelizable, reducción de orden al algoritmo, o alguna otra opción. En cuanto a cuán conformes quedan los usuarios, el 50 % de los que más escuchaba la radio afirma que el sistema sí funciona, escuchan muchos temas de su gusto y hasta conocieron algunos grupos nuevos. Sin embargo, el otro 50 % difiere de las predicciones y opinan que el universo de temas es muy poco variado, y por eso no se quedan conectados mucho tiempo. Esto se debe a que si los mismos usuarios se conectan siempre, pueden sin querer apropiarse de la lista de reproducción, y cuando entre un nuevo usuario le cueste mucho ver reflejado sus gustos en ella. En resumen, se considera que se cumplieron los objetivos planteados, aún cuando la predicción no es del todo efectiva, pero se identifican varias mejoras a estudiar. 7.3. Trabajo futuro Una primera alternativa es mejorar la gestión de la página. Las cuentas se manejan creando un archivo por cada una, y escribiendo en él toda su información. Las canciones se escriben en un archivo de manera ordenada. Todo esto se puede implementar usando bases de datos SQL como MySQL, PostgreSQL, entre otros, lo que permitiría gestionar de manera eficiente tanto las cuentas de usuarios, canciones y evaluaciones. 62 También se puede mejorar la seguridad del sistema, ya que, por ejemplo, las evaluaciones son ingresadas mediante peticiones tipo GET a las páginas PHP, por tanto, cualquier usuario podría usar dicha debilidad para alterar la información. No existe un método automático para confirmar que el índice de la canción evaluada, sea efectivamente válido. Además, se puede desarrollar algún sistema para informar sobre problemas en alguna canción (incompleta, con ruidos, muy larga, entre otras). Se puede estudiar modificaciones al algoritmo predictivo, creando más clasificaciones a las actuales similar y disímil, o acotando su rango. De esta manera sería menos drástica la categorización y más eficiente la predicción, ya que si bien no es errada la hipótesis “a una persona que le gusta cierto estilo es probable que no le guste otro”, no se puede asumir cierta estrictamente y eso sucede al tener sólo las dos categorías. Buscar alternativas para optimizar el orden del algoritmo mejoraría notablemente su escalabilidad. Además, al desarrollarlo en Python, un lenguaje de alto nivel, no se escatima en recursos ni se optimiza el tiempo de ejecución. Por tanto, reescribirlo en un lenguaje de bajo nivel, como C o C++, podría permitir mejoras notables. Paralelizar el algoritmo y la selección de la siguiente canción es otra posible mejora a estudiar, el cálculo de las matrices W (sec. 4.3.1), WT y W 0 (sec. 4.3.2) se puede realizar en varios procesos paralelos, y mientras una canción se reproduce, se puede actualizar la predicción y la lista de reproducción. También se puede combinar el filtrado colaborativo con otro tipo, por ejemplo, el por contenido. De esta manera, se podría identificar el artista de las canciones, la trayectoria del artista, época de la canción, idioma, nacionalidad, estilo, entre otras. Esto permitiría reconocer otro tipo de similitudes entre los usuarios, no tan sólo las evaluaciones en común de canciones en particular. Por ejemplo, si a alguien le gusta la música andina, podrían ser considerados similares a otros que les guste la música en español, de artistas latinoamericanos, de folclore cercano, música tradicional, tipos de instrumentos, o alguna otra característica. Otras opciones a mejorar son cómo agregar novedad a la lista, cómo enfrentar el que usuarios se apoderen del estilo de la radio, cómo evitar el inicio frío, métodos de reducción de orden para las matrices, implementar un sistema de evaluación a gran escala, y cualquier mejora posible que cause algún efecto en el rendimiento de la radio es digno de ser estudiado. 63 Bibliografía [1] Joseph F. McCarthy, Theodore D. Anagnost. “MUSICFX: An Arbiter of Group Preferences for Computer Supported Collaborative Workouts”, Proceedings of the 1998 ACM Conference on Computer Supported Cooperative Work (CSCW ’98). [2] David B. Hauver, James C. French. “Flycasting: Using Collaborative Filtering to Generate a Playlist for Online Radio”, Proc. Int. Conf. Web Delivering of Music (2001). [3] Josephine Griffith and Colm O’ Riordan. “Collaborative Filtering”, IT Centre, NUI, Galway (2000). [4] Dan Cosley, Joseph A. Konstan, John Riedl. “PolyLens: A recommender system for groups of users”, Proceedings of the European Conference on Computer-Supported Cooperative Work (2001). [5] Òscar Celma. “Music Recomendation And Discovery”, Springer-Verlag Berlin Heidelberg Dordrecht London New York (2010). [6] Judith Masthoff. “Selecting a sequence of television items to suit a group of viewers. User Modeling and User-Adapted Interaction”, University Of Brighton, UK (2004). [7] Loren Terveen and Will Hill. “Beyond Recommender Systems: Helping People Help Each Other”, In HCI In The New Millennium, Jack Carroll, ed., Addison-Wesley (2001). [8] Página oficial de Icecast, www.icecast.org. [9] Página oficial de Wimpy, www.wimpyplayer.com. [10] Página oficial de ID3, www.id3.org. [11] Escuela w3, www.w3schools.com. [12] Documentación de Python, www.python.org/doc/. [13] Proyecto LAME, http://lame.sourceforge.net/. 64 Anexo Códigos de Python Listing 1: predictop.py 1 2 import copy import os 3 4 5 6 7 8 9 10 # Funcion recursiva Crea Matriz def create_matrix (shape , value = 0): """ Funcion crea matriz (Tamano , VI): shape : tupla con dimensiones de matriz , value : valor inicial , parametrizado en 0""" if not shape : return value 11 12 return [ create_matrix ( shape [1:] , value ) for _ in xrange ( shape [0])] 13 14 15 16 17 18 19 20 21 # Funcion calcula MDS def MDS(x1 , x2 , norm = 5.0): """ Calcula el MDS ( Distancia Media Cuadratica ) entre 2 diccionarios (x1 , x2 , norm) x1: diccionario 1 x2: diccionario 2 norm: para normalizar , depende del valor maximo en diccionarios , parametrizado en 5""" enambos = 0.0 sdif = 0.0 22 23 24 25 26 27 28 # Cancion por diccionario ( usuario ) for track in x1: # Si ambos evaluaron el tema if x1[ track ] != 0 and x2[ track ] != 0: sdif += pow(x1[ track ]/ float (norm) - x2[ track ]/ float (norm), 2) enambos += 1.0 29 30 31 32 33 34 35 # Si no hubo coincidencias if enambos == 0: return 1.0 # Si las hubo else: return sdif/ enambos 36 37 38 39 40 # Funcion Calcula Pesos def CalculaPesos (X): """ Obtiene Matriz ( lista de listas ) con los pesos de usuarios parecidos X: lista de diccionarios con tuplas de evaluaciones ( track : eval)""" 41 42 print " CalculaPesos () ..." 43 44 45 # Matriz cuadrada para calcular pesos (N usuarios X N usuarios ) PesosPrediccion = create_matrix (( len(X), len(X)), 1) 46 47 48 # Calcular Matriz Mds parecido usuarios for i, inlista in enumerate (X): 65 for j, dicts in enumerate (X): 49 50 # Mismo usuario if i == j: continue # Si ya se calculo la pareja de usuarios elif PesosPrediccion [j][i] != 1: PesosPrediccion [i][j] = PesosPrediccion [j][i] # Si no se ha calculado esta pareja else: PesosPrediccion [i][j] = 1 - MDS(inlista , dicts , 5.0) 51 52 53 54 55 56 57 58 59 60 61 return PesosPrediccion 62 63 64 65 66 67 68 # Funcion para predecir def Predict (X, P, U = 0.5): """ Predice evaluaciones faltantes en base a Pesos calculados 1-MDS , (X, P, U) X: Lista de diccionarios c/ evals de usuarios en tuplas P: Matriz de Pesos 1-MDS U: Umbral [0 -1[ , parametrizado 0.5 """ 69 70 print " Predict () ..." 71 72 73 74 75 76 77 POSnum POSden NEGnum NEGden nPOS = nNEG = = = = = 0 0 0.0 0.0 0.0 0.0 78 79 80 # Copia para ir actualizando final sin modificar original Xfinal = copy. deepcopy (X) 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 # i indice de usuario en lista ( diccionario ) for i, Xdict in enumerate (X): # tupla en diccionario (del track evaluado ) for inXdict in Xdict : # Si evaluacion presente , no predice if Xdict [ inXdict ] != 0: continue else: # j indice de peso de cada usuario con Xdict ( usuario ) for j, ipesos in enumerate (P[i]): # Si el peso es con si mismo if i == j: continue # Si no hay peso elif ipesos < 0.2: continue # Si es mayor a umbral (POS) elif ipesos >= U and X[j][ inXdict ] != 0: POSnum += ipesos *X[j][ inXdict ] POSden += ipesos nPOS += 1 # Si es menor a umbral (NEG) elif ipesos < U and X[j][ inXdict ] != 0: 66 NEGnum += ipesos *X[j][ inXdict ] NEGden += ipesos nNEG += 1 # Otro caso , sigue (X[j][ inXdict ] == 0) else: continue 105 106 107 108 109 110 111 # POS: Si no hubo mayores a umbral , POSden puede ser cero if POSden < 0.2: # peso menor 0.36 POSdiv = POSnum else: POSdiv = POSnum / POSden 112 113 114 115 116 117 # NEG: Si no hubo menores a umbral , NEGden puede ser cero if NEGden < 0.2: # peso menor 0.36 NEGdiv = NEGnum else: NEGdiv = NEGnum / NEGden 118 119 120 121 122 123 # Cuando pesos son cero , no hay nPOS nNEG (no debiera ocurrir ) if nPOS == 0 and nNEG == 0: predicha = 0 # Normalmente hay al menos un peso else: predicha = ( POSdiv *nPOS + (6 - NEGdiv )*nNEG)/( nPOS + nNEG) 124 125 126 127 128 129 130 # Guarda prediccion Xfinal [i][ inXdict ] = predicha POSnum = 0.0 POSden = 0.0 NEGnum = 0.0 NEGden = 0.0 nPOS = 0 nNEG = 0 131 132 133 134 135 136 137 138 139 140 return Xfinal 141 142 143 144 145 ## Funcion principal def Getpredicciones ( rutausers ): """ Retorna diccionario de username con diccionarios " Ntrack evaluacion " rutausers : ruta al directorio de archivos de usuarios """ 146 147 print " Getpredicciones () ..." 148 149 150 # Crear listado con todos los usuarios allusers = [] 151 152 153 154 155 # Crea lista con todos los usuarios for root ,dirs , files in os.walk( rutausers ): for file in [f for f in files ]: allusers . append (file) 156 157 158 # Lista de usuarios con alguna evaluacion listaUsersEval = [] 159 160 # Lista de diccionarios con evaluaciones en tuplas 67 161 listadict = [] 162 163 164 165 166 167 168 # Por cada usuario for user in allusers : # Abre archivo de usuario fu = open( rutausers +user , ’r’) evals = fu.read () fu. close () 169 170 171 172 173 174 # Transforma lo leido a lista de string , sin pass ni mail ni nulo final evals = evals . split ("\n") evals .pop () # Borra ultimo elemento nulo evals .pop (0) # Borra Password evals .pop (0) # Borra Correo 175 176 177 178 179 if (len( evals ) == 0): continue else: listaUsersEval . append (user) 180 181 182 183 # Transforma cada string en tupla for index , pareval in enumerate ( evals ): evals [ index ] = tuple (int(i) for i in pareval . split (" ")) 184 185 186 # Transforma lista de tuplas a diccionario dictevals = dict( evals ) 187 188 189 190 # Revisar diccionarios listados por tracks que falten # Por cada diccionario en lista for inlista in listadict : 191 192 193 194 195 196 197 # Por cada tupla en diccionario dictevals for indict in dictevals : # Si no esta track if not indict in inlista : # Agregar a diccionario inlista , eval = 0 inlista . setdefault (indict ,0) 198 199 200 201 202 203 204 # Por cada tupla en diccionario inlista for ininlista in inlista : # Si no esta track if not ininlista in dictevals : # Agregar a diccionario dictevals , eval = 0 dictevals . setdefault (ininlista ,0) 205 206 207 # Agrega diccionario al final de la lista listadict . append ( dictevals ) 208 209 PesosPrediccion = CalculaPesos ( listadict ) 210 211 212 # Rellena no evaluadas con prediccion listadict = Predict (listadict , PesosPrediccion , 0.7) 213 214 215 216 # diccionario de username ( listaUsersEval ) con diccionario track :eval ( listadict ) dictUsersEvals = {} 68 217 218 219 # Crea diccionario con username y dict predicciones for i,dicc in enumerate ( listadict ): dictUsersEvals [ listaUsersEval [i]] = dicc 220 221 return dictUsersEvals 222 223 224 225 226 227 228 def TopTracks (Pred , Users , Tol = 3): """ Genera un listado de las canciones mejor evaluadas Pred: diccionario de usuarios con diccionario de track :eval Users : Lista de usuarios conectados Tol: Nota minima que debe tener en todos los usuarios ( defecto = 3) """ 229 230 print " TopTracks () ..." 231 232 233 234 # No hay Usuarios conectados if (len( Users ) == 0): return False 235 236 237 238 239 240 241 242 243 244 # Crea Lista con todos los tracks evaluados listtracks = [] # Por cada usuario for user in Pred: # Por cada track en diccionario de user for track in Pred[user ]: # Si track no esta en lista de tracks , agregar if track not in listtracks : listtracks . append ( track ) 245 246 247 248 249 250 251 # Variable para media media = 0 # Variable para usuarios con eval uTotal = 0 # Crea Lista a guardar Top tracks listTop = [] 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 # Por cada track for track in listtracks : # Por cada usuario en Users for uname in Users : # Si uname no tiene predicciones , no cuenta if uname not in Pred: continue # Carga diccionario de usuario userdict = Pred[ uname ] # Suma evaluacion de track a media media += userdict [ track ] # Cuenta de usuarios uTotal += 1 # Divide por cantidad de usuarios contados media /= uTotal if ( media >= Tol): listTop . append ( track ) media = 0 uTotal = 0 69 273 return listTop 274 275 276 277 278 279 280 281 282 283 def PredictTop ( rutaonline , rutausers , tol =3, rutaPtoF = ""): """ Funcion Final que hace uso de todas las anteriores , guarda listado Toptrack en archivo final retorna False si no hay usuarios , o True si los hay rutaonline : ruta del archivo que contiene listado de usuarios conectados rutausers : ruta al directorio de archivo de usuarios rutaTop : ruta del archivo a escribir lista Top tracks tol: nota minima usada de tolerancia ( defecto = 3) rutaPtoF : ruta del archivo a escribir predicciones """ 284 285 print " PredictToptoFile () ..." 286 287 288 # Obtiene arreglo de diccionarios con predicciones prediccion = Getpredicciones ( rutausers ) 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 # Guarda predicciones en archivo , si se desea if( rutaPtoF != ""): fPtoF = open(rutaPtoF , ’w’) # Por cada usuario en prediccion for user in prediccion : # Escribir usuario e inicio de track :eval fPtoF . write (user+":") # Por cada track por usuario for track in prediccion [user ]: # Escribir pareja track :eval fPtoF . write (str( track )+’=’+str( prediccion [user ][ track ])+’;’) # Escribir termino de track :eval fPtoF . write ("\n") fPtoF . close () 304 305 306 307 308 309 310 # Carga usuarios conectados fonlineusers = open( rutaonline , ’r’) users = fonlineusers .read () fonlineusers . close () users = users . split ("\n") users .pop () # Borra ultimo elemento nulo 311 312 return [ prediccion , TopTracks ( prediccion , users , tol)] 70 Listing 2: ices.py 1 2 3 4 5 6 from string import * import sys import random import os import codecs import predictop 7 8 9 10 def stripnulls (data): " strip whitespace and nulls " return data. replace ("\00", ""). strip () 11 12 # This is just a skeleton , something for you to start with. 13 14 15 16 17 18 19 20 21 songnumber = -1 playlist = "" ruta = "" times = 0 listaTop = [] last20 = [] nousersrand = 1 infotrack = "" 22 23 24 25 26 27 28 29 30 # Function called to initialize your python environment . # Should return 1 if ok , and 0 if something went wrong . def ices_init (): print ’Executing initialize () function .. ’ global playlist global listaTop global prediccion global rutaonline 31 32 33 34 35 # Abrir playlist fplaylist = open("/home/ felipe /ices/ playlist .txt", "a+") playlist = fplaylist .read () playlist = playlist . split ("\n") 36 37 38 39 40 41 42 # Armar lista escaneando ruta for root ,dirs , files in os.walk("/mnt/mp3/ Radio "): for file in [f for f in files if f. lower (). endswith (".mp3")]: if not(os.path.join(root , file) in playlist ): playlist . append (os.path.join(root , file)) fplaylist . write (os.path.join(root , file) + "\n") 43 44 fplaylist . close () 45 46 47 48 # Borra contenido de archivo last20 flast20 = open("/var/www /. data/ last20 ", ’w’) flast20 . close () 49 50 51 52 53 54 55 # Ruta archivo de usuarios conectados rutaonline = "/var/www /. data/ usersonline " # Ruta carpeta cuentas de usuario rutausers = "/var/www /. usuarios /" # Ruta archivo de predicciones rutaPtoF = "/var/www /. data/ predicciones " 71 56 57 58 # Obtiene prediccion [ prediccion , listaTop ] = predictop . PredictTop ( rutaonline , rutausers , 3, rutaPtoF ) 59 60 return 1 61 62 63 64 65 66 # Function called to shutdown your python enviroment . # Return 1 if ok , 0 if something went wrong . def ices_shutdown (): print ’Executing shutdown () function ... ’ return 1 67 68 69 70 71 72 73 74 75 76 77 78 79 # Function called to get the next filename to stream . # Should return a string . def ices_get_next (): print ’Executing get_next () function ... ’ global songnumber global playlist global ruta global times global listaTop global last20 global prediccion global rutaonline 80 81 82 83 84 # Chequea si hay usuarios online fonlineusers = open( rutaonline , ’r’) users = fonlineusers .read () fonlineusers . close () 85 86 87 88 89 90 # Abre archivo para revisar si alguien a evaluado frefresh = open("/var/www /. data/ refresh ", ’r’) refresh = frefresh .read () frefresh . close () print " refresh : " + refresh 91 92 93 94 # Si refresh contiene algo if (len( refresh ) != 0): print " Refrescando Predicciones ..." 95 96 97 98 99 # Ruta carpeta cuentas de usuario rutausers = "/var/www /. usuarios /" # Ruta archivo de predicciones rutaPtoF = "/var/www /. data/ predicciones " 100 101 102 # NECESARIO ACTUALIZAR PREDICCIoN Y TOPTRACKS [ prediccion , listaTop ] = predictop . PredictTop ( rutaonline , rutausers , 3, rutaPtoF ) 103 104 105 106 # Borra contenido de archivo refresh frefresh = open("/var/www /. data/ refresh ", ’w’) frefresh . close () 107 108 109 110 111 # No refresh , pero usuarios conectados elif (len( users ) != 0): # Arregla string a lista users = users . split ("\n") 72 112 users .pop () 113 114 115 # Actualiza TopTracks listaTop = predictop . TopTracks ( prediccion , users , 3) 116 117 118 119 # Si no refresh y no usuarios online else: listaTop = False 120 121 122 123 # Borra listado de archivos conectados fonlineusers = open( rutaonline , ’w’) fonlineusers . close () 124 125 126 127 128 129 130 if listaTop : nousersrand = 4 print "Hay usuarios conectados " else: nousersrand = 1 print "No hay usuarios conectados " 131 132 133 134 135 136 137 while True: if ( times %nousersrand == 0): songnumber = random . randrange (0, len( playlist )) else: songnumber = random . randrange (0, len( listaTop )) songnumber = int( listaTop [ songnumber ]) 138 139 times += 1 140 141 142 143 # Si hay mas de 20 canciones reproducidas , olvidar la primera if (len( last20 ) >20): last20 .pop (0) 144 145 146 147 148 149 150 151 152 # Si la cancion es de las ultimas 20 reproducidas , buscar otra if songnumber in last20 : print str( songnumber )+" ya fue tocada ." continue # Si no ha sido reproducida ultimamente else: last20 . append ( songnumber ) break 153 154 155 156 # Ruta de cancion a reproducir ruta = playlist [ songnumber ] return ruta 157 158 159 160 161 162 163 164 165 # This function , if defined , returns the string you ’d like used # as metadata (ie for title streaming ) for the current song. You may # return null to indicate that the file comment should be used. def ices_get_metadata (): # Abrir mp3 para obtener metadata global ruta global songnumber print ’Executing get_metadata () function ... ’ 166 167 mp3 = open(ruta , "rb") 73 168 169 170 mp3.seek ( -128 ,2) metadata = mp3.read (128) mp3. close () 171 172 173 174 175 176 177 infortrack = null if( metadata [:3] == "TAG"): trackdata = {" title ": stripnulls ( metadata [3:33]) , " artist ": stripnulls ( metadata [33:63]) , " album ": stripnulls ( metadata [63:93]) } infotrack = trackdata [" artist "] + " - " + trackdata [" title "] 178 179 180 # Lista para archivos ya escritos en last20 local20 = [] 181 182 183 184 185 186 187 # Escribir ultimas 20 en archivo last20 flast20 = open("/var/www /. data/ last20 ", ’r’) local20 = flast20 .read () flast20 . close () local20 = local20 . split ("\n") local20 .pop () 188 189 190 if (len( local20 ) == 20): local20 .pop (0) 191 192 193 194 195 196 197 198 199 200 201 202 flast20 = open("/var/www /. data/ last20 ", "w") # Escribe lo que tenia antes for track in local20 : writen = track . split (’:’) # Si cancion en archivo no es la actual , escribirla # Evita escribir nuevamente cancion actual if ( writen [0] != str( songnumber )): flast20 . write ( track + "\n") # Escribe nuevo track e info flast20 . write (str( songnumber ) + ’:’ + infotrack + "\n") flast20 . close () 203 204 return infotrack 205 206 207 208 209 210 211 # Function used to put the current line number of # the playlist in the cue file. If you don ’t care about this number # don ’t use it. def ices_get_lineno (): global songnumber print ’Executing get_lineno () function ... ’ 212 213 return songnumber 74 Archivos de Configuración XML Listing 3: icecast.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <icecast > <limits > <clients >100 </ clients > <sources >2</ sources > <threadpool >5</ threadpool > <queue -size >524288 </queue -size > <client - timeout >30 </client - timeout > <header - timeout >15 </header - timeout > <source - timeout >10 </source - timeout > <!-- If enabled , this will provide a burst of data when a client first connects , thereby significantly reducing the startup time for listeners that do substantial buffering . However , it also significantly increases latency between the source client and listening client . For low - latency setups , you might want to disable this. --> <burst -on - connect >1</burst -on - connect > <!-- same as burst -on -connect , but this allows for being more specific on how much to burst . Most people won ’t need to change from the default 64k. Applies to all mountpoints --> <burst -size >65535 </ burst -size > </limits > 22 23 24 25 26 27 <authentication > <!-- Sources log in with username ’source ’ --> <source -password >source </ source -password > <!-- Relays log in username ’relay ’ --> <relay -password >relay </ relay -password > 28 29 30 31 32 <!-- Admin logs in with the username given below --> <admin -user >admin </ admin -user > <admin -password >nimda </ admin -password > </ authentication > 33 34 35 36 37 38 <!-- set the mountpoint for a shoutcast source to use , the default if not specified is / stream but you can change it here if an alternative is wanted or an extension is required <shoutcast -mount >/ live.nsv </ shoutcast -mount > --> 39 40 41 42 43 44 45 46 <!-- Uncomment this if you want directory listings --> <!-<directory > <yp -url -timeout >15 </yp -url -timeout > <yp -url > http: // dir.xiph.org/cgi -bin/yp -cgi </yp -url > </directory > --> 47 48 49 50 51 <!-- This is the hostname other people will use to connect to your server . It affects mainly the urls generated by Icecast for playlists and yp listings . --> <hostname >localhost </ hostname > 75 52 53 54 55 56 57 58 59 60 61 62 63 <!-- You may have multiple <listener > elements --> <listen -socket > <port >8000 </ port > <!-- <bind -address >127.0.0.1 </ bind -address > --> <!-- <shoutcast -mount >/ stream </ shoutcast -mount > --> </listen -socket > <!-<listen -socket > <port >8001 </ port > </listen -socket > --> 64 65 66 67 68 <!--<master -server >127.0.0.1 </ master -server >--> <!--<master -server -port >8001 </ master -server -port >--> <!--<master -update -interval >120 </ master -update -interval >--> <!--<master -password >hackme </ master -password >--> 69 70 71 72 73 <!-- setting this makes all relays on - demand unless overridden , this is useful for master relays which do not have <relay > definitions here. The default is 0 --> <!--<relays -on -demand >1 </ relays -on -demand >--> 74 75 76 77 78 79 80 81 <!-<relay > <server >127.0.0.1 </ server > <port >8001 </ port > <mount >/ example .ogg </ mount > <local -mount >/ different .ogg </ local -mount > <on -demand >0 </on -demand > 82 83 84 85 <relay -shoutcast -metadata >0 </ relay -shoutcast -metadata > </relay > --> 86 87 88 89 90 <!-- Only define a <mount > section if you want to use advanced options , like alternative usernames or passwords <mount > <mount -name >/ example - complex .ogg </ mount -name > 91 92 93 <username > othersource </ username > <password > hackmemore </ password > 94 95 96 97 98 99 100 101 102 103 104 105 106 107 <max -listeners >1 </max -listeners > <dump -file >/ tmp/dump - example1 .ogg </ dump -file > <burst -size >65536 </ burst -size > <fallback -mount >/ example2 .ogg </ fallback -mount > <fallback -override >1 </ fallback -override > <fallback -when -full >1 </ fallback -when -full > <intro >/ example_intro .ogg </ intro > <hidden >1 </ hidden > <no -yp >1 </no -yp > <authentication type =" htpasswd "> <option name =" filename " value =" myauth "/> <option name =" allow_duplicate_users " value ="0"/ > </ authentication > 76 108 109 110 <on -connect >/ home/ icecast /bin/stream -start </on -connect > <on - disconnect >/ home/ icecast /bin/stream -stop </on - disconnect > </mount > 111 112 113 114 115 116 117 118 119 120 <mount > <mount -name >/ auth_example .ogg </ mount -name > <authentication type =" url"> <option name =" mount_add " value =" http: // myauthserver .net/ notify_mount .php "/> <option name =" mount_remove " value =" http: // myauthserver .net/ notify_mount .php "/> <option name =" listener_add " value =" http: // myauthserver .net/ notify_listener .php "/> <option name =" listener_remove " value =" http: // myauthserver .net/ notify_listener .php "/> </ authentication > </mount > 121 122 --> 123 124 <fileserve >1 </ fileserve > 125 126 127 128 <paths > <!-- basedir is only used if chroot is enabled --> <basedir >/ usr/ share /icecast2 </ basedir > 129 130 131 132 133 134 135 <!-- Note that if <chroot > is turned on below , these paths must both be relative to the new root , not the original root --> <logdir >/ var/log/icecast2 </ logdir > <webroot >/ usr/ share / icecast2 /web </ webroot > <adminroot >/ usr/ share / icecast2 /admin </ adminroot > <!-- <pidfile >/ usr/ share / icecast2 / icecast .pid </ pidfile > --> 136 137 138 139 140 141 142 143 144 145 146 147 148 149 <!-- Aliases: treat requests for ’source ’ path as being for ’dest ’ path May be made specific to a port or bound address using the "port" and "bind - address " attributes . --> <!-<alias source ="/ foo" dest ="/ bar "/> --> <!-- Aliases: can also be used for simple redirections as well , this example will redirect all requests for http: // server:port / to the status page --> <alias source ="/" dest ="/ status .xsl "/> </paths > 150 151 152 153 154 155 156 157 158 159 160 161 162 163 <logging > <accesslog > access .log </ accesslog > <errorlog > error .log </ errorlog > <!-- <playlistlog > playlist .log </ playlistlog > --> <loglevel >3 </ loglevel > <!-- 4 Debug , 3 Info , 2 Warn , 1 Error --> <logsize >10000 </ logsize > <!-- Max size of a logfile --> <!-- If logarchive is enabled (1) , then when logsize is reached the logfile will be moved to [ error | access | playlist ]. log.DATESTAMP , otherwise it will be moved to [ error | access | playlist ]. log.old. Default is non - archive mode (i.e. overwrite ) --> <!-- <logarchive >1 </ logarchive > --> </logging > 77 164 165 166 167 168 169 170 171 172 173 174 <security > <chroot >0 </ chroot > <!-<changeowner > <user >nobody </ user > <group >nogroup </ group > </ changeowner > --> </security > </icecast > 78 Listing 4: icespy.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version ="1.0"?> <ices:Configuration xmlns:ices =" http: // www. icecast .org/ projects /ices"> <Playlist > <!-- This is the filename used as a playlist when using the builtin playlist handler . --> <File >/home/ felipe /ices/ playlist .txt </File > <!-- Set this to 0 if you don ’t want to randomize your playlist , and to 1 if you do. --> <Randomize >0 </ Randomize > <!-- One of builtin , perl , or python . --> <Type >python </ Type > <!-- Module name to pass to the playlist handler if using perl or python . If you use the builtin playlist handler then this is ignored --> <Module >ices </ Module > <!-- Set this to the number of seconds to crossfade between tracks . Leave out or set to zero to disable crossfading (the default ). <Crossfade >5 </ Crossfade > --> </Playlist > 20 21 22 23 24 25 26 27 28 29 30 31 <Execution > <!-- Set this to 1 if you want ices to launch in the background as a daemon --> <Background >0 </ Background > <!-- Set this to 1 if you want to see more verbose output from ices --> <Verbose >0 </ Verbose > <!-- This directory specifies where ices should put the logfile , cue file and pid file (if daemonizing ). Don ’t use /tmp if you have l33t h4x0rz on your server . --> <BaseDirectory >/tmp </ BaseDirectory > </ Execution > 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <Stream > <Server > <!-- Hostname or ip of the icecast server you want to connect to --> <Hostname >127.0.0.1 </ Hostname > <!-- Port of the same --> <Port >8000 </Port > <!-- Encoder password on the icecast server --> <Password >source </ Password > <!-- Header protocol to use when communicating with the server . Shoutcast servers need "icy", icecast 1.x needs " xaudiocast ", and icecast 2.x needs "http". --> <Protocol >http </ Protocol > </ Server > 46 47 48 49 50 51 52 53 54 55 <!-- The name of the mountpoint on the icecast server --> <Mountpoint >/ RaDIE .mp3 </ Mountpoint > <!-- The name of the dumpfile on the server for your stream . DO NOT set this unless you know what you ’re doing . <Dumpfile >ices.dump </ Dumpfile > --> <!-- The name of you stream , not the name of the song! --> <Name >RaDIE </ Name > <!-- Genre of your stream , be it rock or pop or whatever --> 79 56 57 58 59 60 61 62 63 <Genre >* </ Genre > <!-- Longer description of your stream --> <Description > Prueba de Radio </ Description > <!-- URL to a page describing your stream --> <URL > http: //152.74.23.87 :8000 / RaDIE .mp3 </URL > <!-- 0 if you don ’t want the icecast server to publish your stream on the yp server , 1 if you do --> <Public >0</ Public > 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <!-- Stream bitrate , used to specify bitrate if reencoding , otherwise just used for display on YP and on the server . Try to keep it accurate --> <Bitrate >128 </ Bitrate > <!-- If this is set to 1, and ices is compiled with liblame support , ices will reencode the stream on the fly to the stream bitrate . --> <Reencode >0</ Reencode > <!-- Number of channels to reencode to , 1 for mono or 2 for stereo --> <!-- Sampe rate to reencode to in Hz. Leave out for LAME ’s best choice <Samplerate >44100 </ Samplerate > --> <Channels >2 </ Channels > </Stream > </ ices:Configuration > 80 Archivos Javascript Listing 5: funciones.js 1 // Funciones javascript 2 3 4 var shorttime = 1000; // 1 segundo var longtime = 5000; // 5 segundos 5 6 7 8 9 10 11 12 13 14 15 16 17 // Para obtener una cookie function getCookie ( c_name ){ var i,x,y, ARRcookies = document . cookie . split (";"); for (i=0;i< ARRcookies . length ;i++) { x= ARRcookies [i]. substr (0, ARRcookies [i]. indexOf ("=")); y= ARRcookies [i]. substr ( ARRcookies [i]. indexOf ("=")+1); x=x. replace (/^\s+|\s+$/g,""); if (x== c_name ) return unescape (y); } } 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 // Al abrir ventana function openpage (){ var username = getCookie (" uRaDIE "); if ( username != null && username !=""){ if ( window . XMLHttpRequest ) {// code for IE7+, Firefox , Chrome , Opera , Safari xmlhttp =new XMLHttpRequest (); } else {// code for IE6 , IE5 xmlhttp =new ActiveXObject (" Microsoft . XMLHTTP "); } xmlhttp . onreadystatechange = function () { if ( xmlhttp . readyState ==4 && xmlhttp . status ==200) { // Hacer algo con respuesta de servidor // document . getElementById (" myDiv "). innerHTML = xmlhttp . responseText ; } } xmlhttp .open("GET"," listening .php? uname ="+username ,true); xmlhttp .send (); } } 43 44 45 46 47 48 49 50 51 // Al cerrar ventana function closepage (){ var username = getCookie (" uRaDIE "); if ( username != null && username !=""){ if ( window . XMLHttpRequest ) {// code for IE7+, Firefox , Chrome , Opera , Safari xmlhttp =new XMLHttpRequest (); } 81 else {// code for IE6 , IE5 xmlhttp =new ActiveXObject (" Microsoft . XMLHTTP "); } xmlhttp . onreadystatechange = function () { if ( xmlhttp . readyState ==4 && xmlhttp . status ==200) { // Hacer algo con respuesta de servidor // document . getElementById (" myDiv "). innerHTML = xmlhttp . responseText ; } } xmlhttp .open("GET"," notlistening .php? uname ="+username ,true); xmlhttp .send (); 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 function checkForm (id , rb){ var id = document . forms [id ]; var rb = checkRadio (id[rb ]); return rb; } function checkRadio (id){ var x = 0; var value = null; while(x<id. length ){ if(id[x]. checked ) value = id[x]. value ; x++; } return value == null ? 0 : value ; } 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 function sendEval (ID , form , rbname ){ var evaluation = checkForm (form , rbname ); if ( window . XMLHttpRequest ) {// code for IE7+, Firefox , Chrome , Opera , Safari xmlhttp =new XMLHttpRequest (); } else {// code for IE6 , IE5 xmlhttp =new ActiveXObject (" Microsoft . XMLHTTP "); } xmlhttp . onreadystatechange = function () { if ( xmlhttp . readyState ==4 && xmlhttp . status ==200) { // Hacer algo con respuesta de servidor document . getElementById (ID). innerHTML = xmlhttp . responseText ; } } xmlhttp .open("GET","eval.php?eval="+ evaluation ,true); xmlhttp .send (); } 106 107 function sendEvalfixed (ID , track , form , rbname ){ 82 var evaluation = checkForm (form , rbname ); if ( window . XMLHttpRequest ) {// code for IE7+, Firefox , Chrome , Opera , Safari xmlhttp =new XMLHttpRequest (); } else {// code for IE6 , IE5 xmlhttp =new ActiveXObject (" Microsoft . XMLHTTP "); } xmlhttp . onreadystatechange = function () { if ( xmlhttp . readyState ==4 && xmlhttp . status ==200) { // Hacer algo con respuesta de servidor document . getElementById (ID). innerHTML = xmlhttp . responseText ; } } xmlhttp .open("GET"," evalfixed .php?eval="+ evaluation +"& track ="+track ,true); xmlhttp .send (); 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 } 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 // jQuery y AJAX // para refrescar artista - canción $( document ). ready ( function (){ var j = jQuery . noConflict (); j( document ). ready ( function () { j(". trackInfo "). everyTime (longtime , function (i){ j.ajax ({ url: " getTrackInfo .php", cache : false , success : function (html){ j(". trackInfo ").html(html); } }) }) }); }); // para refrescar evaluación $( document ). ready ( function (){ var j = jQuery . noConflict (); j( document ). ready ( function () { j(". evalInfo "). everyTime (longtime , function (i){ j.ajax ({ url: " getEval .php", cache : false , success : function (html){ j(". evalInfo ").html(html); } }) }) }); }); // para refrescar duración $( document ). ready ( function (){ 83 var j = jQuery . noConflict (); j( document ). ready ( function () { j(". duration "). everyTime (shorttime , function (i){ j.ajax ({ url: " getTime .php", cache : false , success : function (html){ j(". duration ").html(html); } }) }) }); 164 165 166 167 168 169 170 171 172 173 174 175 176 177 }); 84 Archivos web de Radio Online Listing 6: index.php 1 2 <?php session_start (); ?> <html > 3 4 <head > 5 <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> <script language =" javascript " src="jquery -1.2.6. min.js" ></script > <script language =" javascript " src=" jquery .timers -1.0.0. js" ></script > <script type="text/ javascript " src=" funciones .js" ></script > </head > 6 7 8 9 10 11 12 13 <body onload =" openpage ()" onunload =" closepage ()"> <h1 >RaDIE </h1 > <p> Evalúa canciones para una mejor predicción </p> 14 15 16 17 18 19 <table > <tr > <td align =" center "> <!-- Usando Wimpy Player --> <!-- START WIMPY BUTTON : Standard HTML --> 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <object classid =" clsid :D27CDB6E -AE6D -11cf -96B8 -444553540000 " codebase ="http :// download . macromedia .com/pub/ shockwave /cabs/ flash / swflash .cab# version =8 ,0 ,0 ,0" width ="35" height = "35" id=" wimpybutton229 "> <param name=" movie " value ="http ://152.74.23.87/ Embedded / WimpyButtonv4 .1.7/ wimpy_button .swf" /> <param name="loop" value =" false " /> <param name="menu" value ="true" /> <param name=" quality " value ="high" /> <param name=" bgcolor " value ="# FFFFFF " /> <param name=" wmode " value =" transparent " /> <param name=" flashvars " value =" theFile =http %3A %2F %2F152 %2E74 %2E23 %2E87 %3A8000 %2FRaDIE %2Emp3 & autoplay =yes& bufferAudio =10& icecast =300& wimpyReg =& myid= wimpybutton229 " /> <embed src="http ://152.74.23.87/ Embedded / WimpyButtonv4 .1.7/ wimpy_button .swf" flashvars =" theFile =http %3A %2F %2F152 %2E74 %2E23 %2E87 %3A8000 %2FRaDIE %2Emp3& autoplay =yes& bufferAudio =10& icecast =300& wimpyReg =& myid= wimpybutton229 " width ="35" height ="35" bgcolor ="# FFFFFF " loop=" false " menu="true" quality ="high" name=" wimpybutton229 " align =" middle " allowScriptAccess =" sameDomain " type=" application /x-shockwave - flash " pluginspage ="http :// www. macromedia .com/go/ getflashplayer " wmode =" transparent " /></object > <!-- END WIMPY BUTTON : Standard HTML --> </td > <td align =" center "> <div class =" duration " ></div > </td > <tr > </table > <!-- AJAX para refrescar la canción --> <div class =" trackInfo ">Cargando : Artista - Canción </div ></br > 39 40 41 42 <!-- Ingreso Usuario --> <!-- Evaluar ... --> <? if (isset ( $_COOKIE [" uRaDIE "])): ?> 85 <form id=" fevals " action =""> <table > <tr > <td > <table > <tr > 43 44 45 46 47 48 <td align =" center "> <input type=" radio " name="eval" value ="1" /> </td > <td align =" center "> <input type=" radio " name="eval" value ="2" /> </td > <td align =" center "> <input type=" radio " name="eval" value ="3" /> </td > <td align =" center "> <input type=" radio " name="eval" value ="4" /> </td > <td align =" center "> <input type=" radio " name="eval" value ="5" /> </td > 49 50 51 52 53 </tr > <tr > 54 55 <td <td <td <td <td 56 57 58 59 60 align =" center " >1</td > align =" center " >2</td > align =" center " >3</td > align =" center " >4</td > align =" center " >5</td > </tr > </table > 61 62 </td > <td > 63 64 <input type=" button " value =" Enviar " onclick =" sendEval (’ IDevalInfo ’, ’fevals ’, ’eval ’)"/> 65 </td > </tr > </table > </form > Evaluando como: <?= $_COOKIE [" uRaDIE "]?></br > <div id=" IDevalInfo " class =" evalInfo ">Cargando evaluación ... </div ></br > 66 67 68 69 70 71 72 <a href=" last20 .php" target =" _blank ">Últimas 20 </a></br ></br > 73 74 <a href=" signout .php">Cerrar Sesión </a></br > 75 76 77 <? else: ?> <form action =" ingresar .php" method ="post"> <table > <tr > <td > Usuario : </td > <td > <input type="text" name="user" /> </td > </tr > <tr > <td > Contraseña : </td > <td > <input type=" password " name="pass" /></td > </tr > </table > <input type=" submit " /> </form > <a href=" newuser .html">Nuevo Usuario </a></br > 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 <? endif ?> 86 93 94 </br > <a href="FAQ.html" target =" _blank ">Preguntas Frecuentes </a></br > 95 96 97 </body > 98 99 </html > 87 Listing 7: newuser.html 1 <html > 2 3 <head > 4 <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> </head > 5 6 7 8 9 <body > <h1>RaDIE </h1> <h2>Nuevo Usuario </h2> 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <form action =" nuevousuario .php" method ="post"> <table > <tr> <td >Nombre de Usuario : </td> <td > <input type="text" name=" username " /> </td > </tr> <tr> <td >Contraseña : </td > <td > <input type=" password " name=" password " /></td> </tr> <tr> <td >Repita Contraseña : </td> <td > <input type=" password " name=" passwordcheck " /></td > </tr> <tr> <td >Mail: </td> <td > <input type="text" name="mail" /></td> </tr> </ table > <input type=" submit " /> </form > 32 33 34 </body > 88 Listing 8: FAQ.html 1 <html > 2 3 4 <head > <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> 5 6 7 8 9 </head > <body > <h1>RaDIE </h1> <h2>Preguntas Frecuentes </h2> 10 11 12 <h4>¿Qué es esto?</h4> <p>Es parte del desarrollo de una Biblioteca de Música Colaborativa Online , como proyecto memoria UdeC.</p> 13 14 15 16 <h4>¿En qué consiste ?</h4> <p>Es una Radio Online que adapta la lista de reproducción de acuerdo a los gustos personales de los usuarios conectados .</p> <p>Esto se logra comparando todas las evaluaciones de todos los usuarios y prediciendo las que faltan , por lo que es muy importante que evalúen hartas canciones .</p> 17 18 19 20 <h4>¿Y cómo evalúo ?</h4 > <p>Para evaluar alguna canción , basta con crearse una cuenta de usuario .</p> <p>La escala de evaluación es de 1 a 5, siendo 1 "Muy Mala" y 5 "Muy Buena "</p> 21 22 23 24 <h4>Creo que no está adaptando mucho a mis gustos ... </h4> <p>Si has evaluado pocas canciones , es muy difícil predecir tu gusto , por lo tanto sonarán muchas canciones al azar.</p> <p>Recuerda que la predicción se logra comparando muchas evaluaciones , si tu tienes pocas , no se puede predecir bien.</p> 25 26 27 <h4>¿El nombre de usuario es sensible a Mayúsculas ?</h4> <p>Sí , y los otros campos también .</p> 28 29 30 <h4>¿La clave está encriptada ?</h4> <p>Sí , este sitio no almacena las claves directamente , sólo sus md5.</p> 31 32 33 <h4>¿Me enviarán basura al correo ?</h4> <p>No , se pide en caso de necesitar contactar a los usuarios .</p> 34 35 36 37 <h4>¿Por qué dice en Predicción un 0? </h4 > <p>En Predicción aparece la nota que la radio predice que le pondrás a la canción actual .</ p> <p>Si nadie la ha evaluado , o no has evaluado nada en común con otro usuario , es imposible predecir .</p> 38 39 40 <h3>CONTACTO </h3> <p>Para consultas , denuncias de errores , cambio contraseña , correo u otro motivo , escribir a <a href=" mailto : contactoradie@gmail .com">contactoradie@gmail .com </a>.</p> 41 42 </body > 89 Listing 9: nuevousuario.php 1 <html > 2 3 <head > 4 <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> </head > 5 6 7 8 9 10 11 12 13 14 15 16 17 <body > <h1 >RaDIE </h1 > <h2 > Creando Usuario </h2 > <?php $rutauser = ". usuarios /" . $_POST [" username "]; // Validar Nombre de Usuario if ( $_POST [" username "] == NULL){ echo "Debe Ingresar Nombre De Usuario "; echo " </br >"; echo ’<a href =" newuser .html"> Volver a Crear Usuario </a>’; } 18 19 20 21 22 23 24 // Revisa Existencia y Crear Archivo de Usuario else if (! $fuser = fopen ($rutauser , "x")){ echo " Usuario Ya Existe "; echo " </br >"; echo ’<a href =" newuser .html"> Volver a Crear Usuario </a>’; } 25 26 27 28 29 30 31 32 33 // Validar las Contraseñas else if ( $_POST [" password "] == NULL){ fclose ( $fuser ); unlink ( $rutauser ); echo "Debe ingresar contraseña "; echo " </br >"; echo ’<a href =" newuser .html"> Volver a Crear Usuario </a>’; } 34 35 36 37 38 39 40 41 else if( $_POST [" password "] != $_POST [" passwordcheck "]){ fclose ( $fuser ); unlink ( $rutauser ); echo " Claves No Coinciden "; echo " </br >"; echo ’<a href =" newuser .html"> Volver a Crear Usuario </a>’; } 42 43 44 45 46 47 48 49 50 // Validar Correo else if( $_POST ["mail"] == NULL){ fclose ( $fuser ); unlink ( $rutauser ); echo "Debe Ingresar Correo Electrónico "; echo " </br >"; echo ’<a href =" newuser .html"> Volver a Crear Usuario </a>’; } 51 52 53 54 55 else{ // Encriptar Contraseñas con MD5 y Guardarla en Archivo fwrite ($fuser , md5( $_POST [" password "])."\n"); 90 // Guarda Correo en Archivo fwrite ($fuser , $_POST ["mail"]."\n"); 56 57 58 echo " Usuario Creado con Éxito ."; 59 } 60 61 62 63 64 65 66 fclose ( $fuser ); ?> </br > </br > <a href="http :// hopper .die.udec.cl">Volver a Escuchar RaDIE </a> 67 68 </body > 91 Listing 10: ingresar.php 1 <?php // Crear cookie $expire = time () + 60*60*24*30; // Un Mes de expiración $username = $_POST ["user"]; setcookie (" uRaDIE ", $username , $expire ); 2 3 4 5 6 ?> 7 8 <html > 9 10 <head > 11 <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> </head > 12 13 14 15 16 <body > <h1 >RaDIE </h1 > <h2 > Iniciando Sesión </h2 > 17 18 <?php // $rutauser = "/ home/ felipe / RaDIE / usuarios /". $rutauser = ". usuarios /". $_POST ["user"]; // Revisa si Usuario Existe if(! $fuser = fopen($rutauser , r)){ setcookie (" uRaDIE ", "", time () -3600); exit(" Usuario No Existe "); } 19 20 21 22 23 24 25 26 // Revisa Contraseña Correcta $clave = fread ($fuser , "32"); if( $clave != md5( $_POST ["pass"])){ setcookie (" uRaDIE ", "", time () -3600); exit(" Contraseña Incorrecta "); } 27 28 29 30 31 32 33 fclose ( $fuser ); 34 35 ?> 36 37 38 Bienvenido <?php echo $_POST ["user"]; ?><br /> Puedes empezar a evaluar las canciones . <br /> 39 40 <a href="http :// hopper .die.udec.cl">Volver a Escuchar RaDIE </a> 41 42 43 </body > </html > 92 Listing 11: eval.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php session_start (); $rutauser = ". usuarios /". $_COOKIE [" uRaDIE "]; if( $_GET ["eval"]== NULL) echo " ERROR : Debe Ingresar Evaluación ."; else if( $_GET ["eval"] < 1 || $_GET ["eval"] > 5) echo " ERROR : Evaluación Inválida "; else if (! isset( $_SESSION [" track "])) echo " ERROR : No hay canción para evaluar "; else{ $fdatauser = fopen($rutauser , "r") or exit("No se encuentra el archivo del usuario a leer."); $datauser = fread( $fdatauser , filesize ( $rutauser )) or exit("No se pudo leer archivo de usuario ."); $datauser = split("\n", $datauser ); array_pop ( $datauser ); // Borra último elemento después del último salto de línea ( Nada) fclose ( $fdatauser ); 16 17 18 19 $fuser = fopen ($rutauser , "w") or exit("No se encuentra el archivo del usuario a escribir ."); fwrite ($fuser , array_shift ( $datauser )."\n") or exit("No se pudo escribir Password ."); // Password fwrite ($fuser , array_shift ( $datauser )."\n") or exit("No se pudo escribir Correo ."); // Correo 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // buscar , cambiar par_eval si existe , o agregar si no , registra cambio en archivo $notindata = true; // flag "No track evaluado " foreach ( $datauser as & $par_eval ){ $par_eval_array = explode (" ", $par_eval ); if ( $par_eval_array [0] == $_SESSION [" track "]){ // Abre archivo prediccion vs. evaluacion ( escribe vieja vs. nueva ) "EE" $fPvsE = fopen(".data/PvsE", ’a’); fwrite ($fPvsE , $_COOKIE [" uRaDIE "]. ’ ’. $_SESSION [" track "]. ’ ’. $par_eval_array [1]. ’ ’. $_GET ["eval"]." EE\n"); fclose ( $fPvsE ); $par_eval_array [1] = $_GET ["eval"]; $notindata = false; $par_eval = $par_eval_array [0]." ". $par_eval_array [1]; break; } } unset ( $par_eval ); 37 38 39 40 // Si no estaba , se agrega track evaluado y se escribe prediccion y eval en archivo if( $notindata ){ array_push ($datauser , $_SESSION [" track "]." ". $_GET ["eval"]); 41 // Abre archivo prediccion vs. evaluacion "PE" $fPvsE = fopen(".data/PvsE", ’a’); fwrite ($fPvsE , $_COOKIE [" uRaDIE "]. ’ ’. $_SESSION [" track "]. ’ ’. $_SESSION [" predict "]. ’ ’. $_GET ["eval"]." PE\n"); fclose ( $fPvsE ); 42 43 44 45 46 47 } 93 // Escribe evaluaciones a archivo de usuario foreach ( $datauser as $par){ fwrite ($fuser , $par."\n") or exit("No se pudo escribir evaluación ."); } fclose ( $fuser ); 48 49 50 51 52 53 // Refrescar con nueva evaluacion $fneweval = fopen(".data/ refresh ", "w"); fwrite ($fneweval , "1"); fclose ( $fneweval ); 54 55 56 57 58 echo "Su evaluación fue: ". $_GET ["eval"]." </br ></br >"; 59 } 60 61 ?> 94 Listing 12: evalfixed.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php session_start (); // $rutauser = "/ home/ felipe / RaDIE / usuarios /". $_COOKIE [" uRaDIE "]; $rutauser = ". usuarios /". $_COOKIE [" uRaDIE "]; if( $_GET ["eval"]== NULL) exit(" ERROR : Debe Ingresar Evaluación ."); else if( $_GET ["eval"] < 1 || $_GET ["eval"] > 5) exit(" ERROR : Evaluación Inválida "); else{ $fdatauser = fopen($rutauser , "r") or exit("No se encuentra el archivo del usuario a leer."); $datauser = fread( $fdatauser , filesize ( $rutauser )) or exit("No se pudo leer archivo de usuario ."); $datauser = split("\n", $datauser ); array_pop ( $datauser ); // Borra último elemento después del último salto de línea ( Nada) fclose ( $fdatauser ); 15 16 17 18 $fuser = fopen ($rutauser , "w") or exit("No se encuentra el archivo del usuario a escribir ."); fwrite ($fuser , array_shift ( $datauser )."\n") or exit("No se pudo escribir Password ."); // Password fwrite ($fuser , array_shift ( $datauser )."\n") or exit("No se pudo escribir Correo ."); // Correo 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // buscar , cambiar par_eval si existe , o agregar si no , registra cambio en archivo $notindata = true; // flag "No track evaluado " foreach ( $datauser as & $par_eval ){ $par_eval_array = explode (" ", $par_eval ); if ( $par_eval_array [0] == $_GET [" track "]){ // Abre archivo prediccion vs. evaluacion ( escribe vieja vs. nueva ) "EE" $fPvsE = fopen(".data/PvsE", ’a’); fwrite ($fPvsE , $_COOKIE [" uRaDIE "]. ’ ’. $_GET [" track "]. ’ ’. $par_eval_array [1]. ’ ’. $_GET ["eval"]." EE\n"); fclose ( $fPvsE ); $par_eval_array [1] = $_GET ["eval"]; $notindata = false; $par_eval = $par_eval_array [0]." ". $par_eval_array [1]; break; } } unset ( $par_eval ); 36 37 38 39 40 41 42 43 44 45 46 47 // Si no estaba , se agrega track evaluado y se escribe prediccion y eval en archivo if( $notindata ){ // Obtener prediccion de $_GET [" track "] // Para obtener prediccion $rutaPred = ".data/ predicciones "; $fPred = fopen($rutaPred , r) or exit("No se encuentra el archivo de predicciones "); do{ $username = fgets ( $fPred ); // Usuario + : // Username en [0] , Diccionario en [1] $username = explode (’:’, $username ); } 95 while( $username [0] != $_COOKIE [" uRaDIE "]); 48 49 // cada pareja de track : evaluacion separada $username [1] = explode (’;’, $username [1]); 50 51 52 // Si no hay prediccion , eval = 0 $eval_predict = 0; foreach ( $username [1] as $track ){ // separa track de evaluacion $track = explode (’=’, $track ); if ( $track [0] == $_GET [" track "]){ $eval_predict = $track [1]; break ; } } fclose ( $fPred ); 53 54 55 56 57 58 59 60 61 62 63 64 array_push ($datauser , $_GET [" track "]." ". $_GET ["eval"]); 65 66 // Abre archivo prediccion vs. evaluacion "PE" $fPvsE = fopen(".data/PvsE", ’a’); fwrite ($fPvsE , $_COOKIE [" uRaDIE "]. ’ ’. $_GET [" track "]. ’ ’. $eval_predict .’ ’. $_GET ["eval"]." PE\n"); fclose ( $fPvsE ); 67 68 69 70 } 71 72 // Escribe evaluaciones a archivo de usuario foreach ( $datauser as $par){ fwrite ($fuser , $par."\n") or exit("No se pudo escribir evaluación ."); } fclose ( $fuser ); 73 74 75 76 77 78 // Refrescar con nueva evaluacion $fneweval = fopen(".data/ refresh ", "w"); fwrite ($fneweval , "1"); fclose ( $fneweval ); 79 80 81 82 83 echo "Su evaluación fue: ". $_GET ["eval"]; 84 } 85 86 ?> 96 Listing 13: getEval.php 1 <?php session_start (); $_SESSION [" predict "] = 0; // Para usuarios que evalúan por primera vez $rutauser = ". usuarios /". $_COOKIE [" uRaDIE "]; $fuser = fopen($rutauser , ’r’) or exit("No se encuentra el archivo del usuario "); $song = fgets ( $fuser ); // Password $song = fgets ( $fuser ); // eMail $song = fgets ( $fuser ) or exit("No has evaluado esta canción ."); // primera evaluación , si hay 2 3 4 5 6 7 8 9 $fueeval = false; // flag para lo anterior while (! feof( $fuser )){ $song = explode (" ", $song ); if ( $song [0] == $_SESSION [" track "]){ $fueeval = true; $eval_actual = $song [1]; break; } 10 11 12 13 14 15 16 17 18 $song = fgets ( $fuser ); } fclose ( $fuser ); 19 20 21 22 // Para obtener predicción $rutaPred = ".data/ predicciones "; $fPred = fopen($rutaPred , r) or exit("No se encuentra el archivo de predicciones "); do{ $username = fgets ( $fPred ); // Usuario + : // Username en [0] , Diccionario en [1] $username = explode (’:’, $username ); } while( $username [0] != $_COOKIE [" uRaDIE "]); 23 24 25 26 27 28 29 30 31 32 // cada pareja de track : evaluación separada $username [1] = explode (’;’, $username [1]); 33 34 35 // Si no hay predicción , eval = 0 $eval_predict = 0; foreach ( $username [1] as $track ){ // separa track de evaluación $track = explode (’=’, $track ); if ( $track [0] == $_SESSION [" track "]){ $eval_predict = $track [1]; break; } } fclose ( $fPred ); $_SESSION [" predict "] = $eval_predict ; 36 37 38 39 40 41 42 43 44 45 46 47 48 if ( $fueeval ) echo " Evaluaste esta canción con un ". $eval_actual ." </br ></br >"; else echo "No has evaluado esta canción .</br >"." Predicción : ". $eval_predict ; 49 50 51 52 53 ?> 97 Listing 14: getTime.php 1 <?php session_start (); $file = fopen ("/tmp/ices.cue", "r") or exit("No se pudo leer .cue"); $info = fread ($file , filesize ("/tmp/ices.cue")); fclose ( $file ); $info = split ("\n", $info ); array_pop ( $info ); // $route = $info [0];// rtrim ( fgets ( $file ), "\n"); // $size = $info [1];// rtrim ( fgets ( $file ), "\n"); // $bitrate = $info [2];// rtrim ( fgets ( $file ), "\n"); $duration = $info [3]; // rtrim ( fgets ( $file ), "\n"); $progress = $info [4]; // rtrim ( fgets ( $file ), "\n"); $idlist = $info [5]; // rtrim ( fgets ( $file ), "\n"); $artist = $info [6]; // rtrim ( fgets ( $file ), "\n"); $title = $info [7]; // rtrim ( fgets ( $file ), "\n"); 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $_SESSION [" track "] = $idlist ; $_SESSION [" artist "] = $artist ; $_SESSION [" title "] = $title ; 17 18 19 20 $dur = split (":", $duration ); $total = $dur [2]*60 + $dur [3]; $time = $total * $progress /100; 21 22 23 24 $time = round ( $time ); $total = round( $total ); $min = floor ( $time /60); $sec = $time %60; 25 26 27 28 29 $_SESSION ["time"] = $time ; $_SESSION [" total "] = $total ; 30 31 32 if ( $total == 0) echo " --:--/--:--"; else echo sprintf (" %’02u: %’02u/ %’02u: %’02u", $min , $sec , $dur [2] , $dur [3]); 33 34 35 36 37 ?> 98 Listing 15: getTrackInfo.php 1 <?php session_start (); 2 3 // Forma el string con informacion artista - canción $trackinfo = $_SESSION [" artist "]." - ". $_SESSION [" title "]; 4 5 6 // Si hay cookie , escribir usuario conectado if ( $_COOKIE [" uRaDIE "]){ $rutaonline = ".data/ usersonline "; $file = fopen( $rutaonline ,"a+"); $usuarios = fread($file , filesize ( $rutaonline )); $usuarios = split("\n",$usuarios ); array_pop ( $usuarios ); 7 8 9 10 11 12 13 14 // Si usuario no esta en archivo online , escribirlo if (! in_array ( $_COOKIE [" uRaDIE "], $usuarios )) fwrite ($file , $_COOKIE [" uRaDIE "]."\n"); 15 16 17 18 fclose ( $file ); 19 } 20 21 echo $trackinfo ; 22 23 ?> 99 Listing 16: last20.php 1 2 3 4 5 <html > <head > <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> <script type="text/ javascript " src=" funciones .js" ></script > </head > 6 7 8 9 10 11 12 13 14 15 16 17 <body > <h1 >RaDIE </h1 > <h2 > Últimas 20 canciones reproducidas </h2 > <p> Debes actualizar la página para refrescar la lista <p> <?php $flast20 = fopen (".data/ last20 ", ’r’) or exit("No se puede abrir last20 "); $last20 = fread ($flast20 , filesize (".data/ last20 ")) or exit("No se puede leer last20 " ); fclose ( $flast20 ); $last20 = split ("\n", $last20 ); array_pop ( $last20 ); $last20 = array_reverse ( $last20 ); 18 19 20 21 22 // Abrir archivo de usuario para buscar evaluación $fuser = fopen (". usuarios /". $_COOKIE [" uRaDIE "], ’r’) or exit("No se encuentra el archivo del usuario "); $song = fgets ( $fuser ); // Password $song = fgets ( $fuser ); // eMail 23 24 25 26 27 28 29 30 31 32 33 // Crea lista con temas y evaluaciones , si hay $HasSongs = false; $allsongs = array (); while (! feof( $fuser )){ $HasSongs = true; $song = fgets ( $fuser ); $song = explode (" ", $song ); array_push ($allsongs , $song ); } fclose ( $fuser ); 34 35 36 37 38 // N para número de track pasado $N = 1; foreach ( $last20 as $track ){ $evaluada = false ; 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 $track = explode (’:’, $track ); echo ’ <b>’.$N.". ". $track [1]." </b>"; echo ’ <form id =" feval ’.$N.’" action ="" > <table > <tr > <td > <table > <tr >’; for ($i =1; $i <6; $i ++) echo ’ <td align =" center "> <input type =" radio " name =" eval ’.$N.’" value =" ’.$i.’" /> </td >’; echo ’ 100 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 </tr > <tr >’; for ($i =1; $i <6; $i ++) echo ’ <td align =" center ">’.$i.’ </td >’; echo ’ </tr > </table > </td > <td > <input type =" button " value =" Enviar " onclick =" sendEvalfixed (\’ID ’.$N.’\’, ’.( int) $track [0]. ’, \’ feval ’.$N.’\’, \’eval ’.$N.’\’)"/> </td > <td >’; // Buscar la canción , si fue evaluada if ( $HasSongs ) foreach ( $allsongs as $song ){ if ( $song [0] == $track [0]){ $evaluada = true; echo ’ <div id =" ID ’.$N.’" style =" vertical - align : middle "> Evaluaste esta canción con un ’. $song [1]. ’ </div >’; } } // Si no fue evaluada if (! $evaluada ) echo ’ <div id =" ID ’.$N.’" style =" vertical - align : middle ">No has evaluado esta canción </div >’; echo ’ </td > </tr > </table > </form >’; 85 $N += 1; 86 } 87 88 89 90 ?> </body > </html > 101 Listing 17: listening.php 1 <?php // Open file $ruta = ".data/ usersonline "; $file = fopen ($ruta ,"a+"); $usuarios = fread($file , filesize ( $ruta )); $usuarios = split("\n",$usuarios ); array_pop ( $usuarios ); 2 3 4 5 6 7 8 // Si usuario no está en archivo online , escribirlo if (! in_array ( $_GET [" uname "], $usuarios )) fwrite ($file , $_GET [" uname "]."\n"); 9 10 11 12 fclose ( $file ); 13 14 ?> 102 Listing 18: notlistening.php 1 <?php // Open file $ruta = ".data/ usersonline "; $file = fopen ($ruta ,"r"); $usuarios = fread($file , filesize ( $ruta )); fclose ( $file ); $usuarios = split("\n",$usuarios ); array_pop ( $usuarios ); 2 3 4 5 6 7 8 9 $file = fopen ($ruta ,"w"); foreach ( $usuarios as $user ){ if ( $user == $_GET [" uname "] ) continue ; else fwrite ($file , $user ."\n"); } 10 11 12 13 14 15 16 17 fclose ( $file ); 18 19 session_destroy (); 20 21 ?> 103 Listing 19: signout.php 1 <?php session_start (); 2 3 ?> 4 5 <html > 6 7 <head > <meta http - equiv ="Content -Type" content ="text/html; charset =UTF -8"/> 8 9 10 </head > 11 12 13 14 15 16 17 18 19 <body > <h1 >RaDIE </h1 > <h2 > Sesión Cerrada </h2 > <?php setcookie (" uRaDIE ", "", time () -3600); session_destroy (); ?> <a href="http :// hopper .die.udec.cl">Volver a Escuchar RaDIE </a> 20 21 </body >