domingo, julio 22, 2012

Renombrando canciones 2.1

Venga, vamos a darnos otra chuta de Python antes de pasar a Javascript...

El otro disco que me bajé tenía las canciones sin los números. Seguramente estarían en los tags id3, pero es algo que me da mucha pereza, así que miré el orden de las pistas en Amazon y me hice otro script ^_^

Este recibe como argumentos dos ficheros, uno con el listado de pistas de Amazon (copiado de la tabla usando ctrl+seleccionar)

  1. Half Remembered Dream
  2. We Built Our Own World
  3. Dream Is Collapsing
  4. Radical Notion
  5. Old Souls
  6. 528491
  7. Mombasa
  8. One Simple Idea
  9. Dream Within A Dream
10. Waiting For A Train
11. Paradox
12. Time

y otro con el nombre de los archivos:

528491.mp3
Dream Is Collapsing.mp3
Dream Within A Dream.mp3
Half Remembered Dream.mp3
Inception Teaser Trailer.mp3
Mind Heist.mp3
Mombasa.mp3
Old Souls.mp3
One Simple Idea.mp3
Paradox.mp3
Projections (bonus track).mp3
Radical Notion.mp3
Time.mp3
Waiting For A Train.mp3
We Built Our Own World.mp3
y se encarga de emparejarlos según cuáles sean más parecidos usando el Longest Common Substring (por lo que debería ser robusto ante pequeños cambios).


#!/usr/bin/env python

import sys
import itertools
import functools
from collections import defaultdict

allchars = (chr(i) for i in range(256))
table = ''.join(x.lower() if x.isalnum() else ' ' for x in allchars)

def memoize(obj):
        cache = obj.cache = {}
        @functools.wraps(obj)
        def memoizer(*args, **kwargs):
                if args not in cache:
                        cache[args] = obj(*args, **kwargs)
                return cache[args]
        return memoizer

@memoize
def lcs(ra, rb):
        if not (ra and rb):
                return 0
        elif ra[0] == rb[0]:
                return 1 + lcs(ra[1:], rb[1:])
        else:
                return max(lcs(ra[1:], rb), lcs(ra, rb[1:]))


def make_all_pairs(group_a, group_b):
        def rank(a, b):
                h = lcs(a.translate(table), b.translate(table))
                return 2.0*h/(len(a) + len(b))

        for ia, a in enumerate(group_a):
                ib, b = max(enumerate(group_b), key=lambda (_, b): rank(a, b))
                yield ia, ib


def format_pairs(number_name, file_ext):
        numbers, track_names = zip(*number_name)
        numbers = map(int, numbers)
        track_names = map(str.strip, track_names)
        file_names, exts = zip(*file_ext)
        for t_i, f_i in make_all_pairs(track_names, file_names):
                print 'mv "%(fname)s.%(ext)s" "%(n)02d - %(tname)s.%(ext)s"' % {
                        'fname' : file_names[f_i],
                        'ext' : exts[f_i],
                        'tname' : track_names[t_i],
                        'n' : numbers[t_i]
                }

tracks, files = (map(str.strip, open(x).readlines()) for x in sys.argv[1:])

number_name = [x.split('.',1) for x in tracks]
file_ext = [x.rsplit('.', 1) for x in files]

format_pairs(number_name, file_ext)

sábado, julio 21, 2012

Renombrando canciones 2.0

Hoy necesitaba mi viejo script para renombrar canciones y el cabroncete me ha dejado un poco tirado, no borraba completamente el autor que estaba repetido en todos los nombres de los ficheros (era "Hans Zimmer & James New Howard" y sólo se cargaba el "Hans").

Como no me apetecía una puta mierda ponerme a depurarlo (el código tiene ya 7 años y los bits se van pudriendo), me he dicho, ¿a ver cómo lo reescribiría siendo un poco más viejo?

El resultado es este:


#!/usr/bin/env python
import sys

allchars = (chr(i) for i in range(256))
table = ''.join(x if x.isalnum() else ' ' for x in allchars)

def remove_common_tokens(tokenized_lines):
        common = reduce(set.__and__, map(set, tokenized_lines))
        is_unique = lambda x: x not in common
        return [filter(is_unique, line) for line in tokenized_lines]

originals = map(str.strip, sys.stdin)
names, extensions = zip(*[x.rsplit('.',1) for x in originals])
tokenized_lines = [x.translate(table).title().split() for x in names]
unique_tokens = remove_common_tokens(tokenized_lines)
numbers = [str(int(x[0])).zfill(2) for x in unique_tokens]
new_names = [' '.join(x[1:]) for x in unique_tokens]

for original, number, new_name, extension in zip(originals, numbers, new_names, extensions):
        print 'mv "' + original + '" "' + number + ' - ' + new_name + '.' + extension + '"'

A tope. Lo que antes me llevaba 88 líneas ahora me lleva 21.  Soy un 25% más vago. Evidentemente tiene un bug (es el precio de ahorrar tanto), pero me la suda.

La mejora más aparente es que he eliminado toda la morralla de eliminar las partes iguales con un método más compacto y agresivo: cogemos todos los tokens comunes (independientemente de dónde estén) y nos los cargamos. El bug está ahí, si da la casualidad de que hay una palabra legítima que se repite en todos los títulos (como podría ser "of" por ejemplo), pues va a ir a tomar por culo también.

El resto sigue siendo más o menos igual, vamos aplicando transformaciones (tokenizar, sacar las extensiones y los números, eliminar partes repetidas, etc.) y al final componemos el resultado.

Algo gracioso que ha cambiado con la edad es que antes sólo usaba comillas dobles para los literales de strings (manías de Java, supongo), ahora suelo usar las simples.

Mi relación con map y filter es un poco de amor odio. Recuerdo que mi primera versión tiraba bastante de map, pero luego lo reescribí para usar list comprehensions. Ahora uso lo que sea más corto, si ya tengo la función definida, pues suelo tirar de map, si tengo que hacer un map y luego un filter seguramente use una comprehension que queda más clara, etc.

Sobre reduce la verdad es que no le suelo encontrar mucho uso, porque las reducciones típicas son sum y str.join y ya están los built-in, pero aquí por ejemplo tenía que encontrar la intersección de unos cuantos sets y me parecía mucho más elegante usar reduce que un bucle con acumulador.

En el próximo post (espero que en una semana o así) seguramente cuelgue algo de código en... ¡Javascript! quién me lo iba a decir a mí después de tantos años.