Música é vida. Eu realmente amo ouvir música, apesar que não o mesmo tipo de música o tempo todo. Mas na maioria das vezes, vai de tudo: Eu gosto de ouvir qualquer uma das músicas que eu tenho aleatoriamente. Mas quando eu estou fazendo algo que precisa de concentração (como escrever esse texto) eu só posso ouvir música "de fundo", ou seja, música que não tem vocal. Por isso ter agrupamentos de músicas é bem útil para esse tipo de situação.
Um jeito de agrupar músicas é por meio de listas de reprodução. Mas os critérios que determinam quais músicas vão em uma lista é bastante subjetivo já que é determinado por mim, um ser humano. Foi isso que me motivou a criar algum jeito de gerar listas de reprodução baseado em critérios variados: nome de pasta, artista, álbum, etc.
Recentemente eu li o incrível livro Python Fluente (de autor brasileiro!) com o objetivo de aprofundar minhas habilidades em python, que se tornou minha linguagem de programação preferida já há algum tempo. Eu aprendi bastante lendo esse livro, mas ainda não tinha praticado nada do que aprendi. Uma das coisas mais interessantes que aprendi foi sobre os métodos especiais que fazem dos objetos em python tão flexíveis. Foi aí que percebi que usando métodos especiais, eu conseguiria criar objetos de lista de reprodução bem flexíveis e ao mesmo tempo praticar esse conceito mágico. Ganha-ganha.
O programa que eu uso para gerenciar minha biblioteca de músicas e também para tocar as músicas é o MPD. Ele suporta requisição de informações sobre as músicas por outros programas e também reproduzir músicas com base em uma lista de reprodução, então foi simples fazer o código de geração de listas de reprodução usando o MPD.
A ideia é a seguinte: Eu quero poder criar uma lista de reprodução especificando o que ela deve conter, e também o que ela não deve, e também poder combinar critérios de outras listas usando operações de E e OU. Isso provavelmente ficará mais claro mais à frente com os exemplos.
O código que implementa essa classe está em mpd_playlist.py
, e contém o
seguinte:
from mpd import MPDClient client = MPDClient() client.timeout = 60 client.idletimeout = None client.connect("localhost", 6600) class MPDPlaylist(): def __init__(self, query, name=''): self.name = name if isinstance(query, self.__class__): self.songs = set(query.songs) elif isinstance(query, set): self.songs = set(query) else: self.songs = self.query_songs(query) def __repr__(self): return f"{self.__class__}({self.name}, {self.songs})" def __or__(self, other): return MPDPlaylist(self.songs.union(other.songs)) def __and__(self, other): return MPDPlaylist(self.songs.intersection(other.songs)) def __neg__(self): return MPDPlaylist(MPDPlaylist({}).songs.difference(self.songs)) def query_songs(self, queries): if queries: args = [] for k,v in queries.items(): args.append(k) args.append(v) return set(song.get('file') for song in client.search(*args)) else: return set(song.get('file') for song in client.listall() if song.get('file')) def write_to_file(self, base): if self.name: with open(f"{base}/{self.name}.m3u", 'w') as f: f.write('\n'.join(self.songs) + '\n') else: print("Can't write playlist with no name!")
Aquele trecho com MPDClient()
no começo é necessário para conectar ao banco
de dados do MPD para posteriormente obter todas as informações sobre as músicas.
Ele é provido pelo pacote python-mpd2.
O método __init__()
, que é chamado quando um novo objeto é criado, pode
receber outra lista de reprodução como seu parâmetro query
e nesse caso a
nova lista apenas as músicas da lista passada. Ele também pode receber um
conjunto de músicas (o que eu ainda nem cheguei a usar, mas fazia sentido
suportar). E finalmente, no caso mais comum, pode receber um dicionário contendo
os parâmetros de busca que vão ser usados para filtrar as músicas do MPD. Para
todos os casos, um nome pode opcionalmente ser passado (necessário para que a
lista possa ser salva).
Então, por exemplo, músicas_paramore = MPDPlaylist({'artist': 'Paramore'})
criaria uma lista de reprodução apenas com músicas do Paramore, e
músicas_paramore2 = MPDPlaylist(músicas_paramore)
poderia ser usado para
criar uma cópia dessa lista. Esse segundo caso não parece tão útil mas é a base
para processar expressões, como veremos.
__repr__()
só é usado para depurar. Ele especifica como o objeto é escrito
na tela, nesse caso, mostrando quais músicas a lista de reprodução contém.
__or__()
e __and__()
são chamadas quando duas listas de reprodução são
combinadas com OU (usando |
) e com E (usando &
), retornando uma lista
que contém a união (músicas das duas listas) e a intersecção (apenas músicas
presentes nas duas) respectivamente. __neg__()
serve para negar a lista
(usando -
), fazendo com que os parâmetros de busca especifiquem o que ela
não deve conter, e portanto que ela contenha tudo menos o que for indicado.
query_songs()
obtém as músicas do MPD com base nos parâmetros de busca e as
salva dentro do objeto.
write_to_file()
salva a lista de reprodução em um arquivo .m3u
para que
possa posteriormente ser lido e as músicas reproduzidas pelo MPD. O nome do
arquivo é o que foi fornecido em __init__
.
Agora, alguns exemplos.
Em outro arquivo, playlists.py
, eu tenho a definição de todas as minhas
listas de reprodução usando a classe MPDPlaylist:
from mpd_playlist import MPDPlaylist as PL saved = PL({'base': "genres"}, "0_Saved") buffer = PL({'base': "buffer"}, "1_Buffer") fvt = PL({'file': "/%"}, "1_FvtAlbums") br = PL({'base': "genres/br"} , "2_Br") classical = PL({'base': "genres/classical"} , "2_Classical") electronic = PL({'base': "genres/electronic"} , "2_Electronic") etc = PL({'base': "genres/etc"} , "2_ETC") instrumental = PL({'base': "genres/instrumental"} , "2_Instrumental") jazz = PL({'base': "genres/jazz"} , "2_Jazz") pop = PL({'base': "genres/pop"} , "2_Pop") post_rock = PL({'base': "genres/post-rock"} , "2_Post-rock") rap = PL({'base': "genres/rap"} , "2_Rap") rock = PL({'base': "genres/rock"} , "2_Rock") soundtrack = PL({'base': "genres/soundtrack"} , "2_Sountrack") common = PL(rock | electronic | pop | post_rock | rap, "1_Common") not_soundtrack = PL(-soundtrack, "2_NotSoundtrack") tdg = PL({'artist': "Three Days Grace"}, "3_TDG") minecraft = PL({'base': "genres/soundtrack/games/%minecraft"}, "3_Minecraft") lotr = PL({'base': "genres/soundtrack/movies/lotr"}, "3_LOTR") background = PL(classical | jazz | instrumental | PL({'artist': "Balmorhea"}) | PL({'artist': "Tycho"}) | PL({'artist': "MASTER BOOT RECORD"}) | PL({'artist': "The Album Leaf"}) | PL({'base': "genres/soundtrack/animes/tatami_galaxy/%ost"}) | PL({'base': "genres/soundtrack/games"}) & -PL({'file': "genres/soundtrack/games/Portal - Still Alive.mp3"}) & -PL({'file': "genres/soundtrack/games/portal_2/3-13_want_you_gone.mp3"}) & -PL({'title': "Here Comes Vi"}), "1_Background")
As primeiras listas criadas são:
saved
, que contém todas as músicas da pasta genres
, essencialmente
minha biblioteca de músicas;buffer
, com todas as músicas da pasta buffer
, que são aquelas que
ainda estou ouvindo para decidir se gosto ou não;fvt
com todas as músicas de pastas começando com %
, que são meus
álbuns favoritosEm seguida são definidas listas pra cada um dos gêneros. Algumas delas,
rock
, electronic
, pop
, post_rock
e rap
são então combinadas
usando o operador OU (|
) para criar a lista common
. Isso significa que
essa lista de reprodução contém as músicas de todas essas listas combinadas.
Aí a lista not_soundtrack
é criada negando a lista soundtrack
, então
aquela contém apenas as músicas não presentes nesta.
A lista tdg
possui apenas músicas do artista Three Days Grace
.
A última lista, background
, combina várias listas definidas anteriormente
como classical
, e também listas anônimas como PL({'artist':
"Balmorhea"})
(que são usadas apenas para criar essa lista, e não serão salvas
como listas independentes, portanto não são armazenadas em variáveis e também
não precisam de um nome como parâmetro), bem como remove músicas específicas
como PL({'file': "genres/soundtrack/games/Portal - Still Alive.mp3"})
.
Não é a sintaxe mais sucinta de todas, mas também não chega a ser extensa e é bastante flexível: serviu para tudo que eu precisava customizando as minhas listas.
Talvez agora você esteja se perguntando como essas listas de reprodução são
escritas em arquivos se elas são apenas criadas e armazenadas em variáveis. A
resposta é: python é demais 😃. Esse é gen_playlists.py
, o código que faz
isso:
#!/bin/python from os.path import expanduser from mpd_playlist import MPDPlaylist import playlists PLD = expanduser("~/.config/mpd/playlists") # All variables defined locally with type MPDPlaylist and with a name will be # contained here playlists = (pl for pl in vars(playlists).values() if isinstance(pl, MPDPlaylist) and pl.name) for playlist in playlists: playlist.write_to_file(PLD)
Como aquele comentário gentil em cima diz, playlists = (pl for pl in
vars(playlists).values() if isinstance(pl, MPDPlaylist) and pl.name)
obtém
todas as variáveis do tipo MPDPlaylist
de playlists.py
, desde que tenham
nome. Aí laço for
itera sobre elas e salva cada uma em um arquivo com seu
nome.
Por fim, eu adicionei uma linha no cron
para executar esse script todo
domingo, atualizando minhas listas de reprodução.
Como nada é perfeito, tem uma lista de reprodução que eu não consegui integrar
nessa infraestrutura e portanto ficou como um script em bash separado 🤢: a
lista newest
. Ela contém as últimas 100 músicas adicionadas à minha
biblioteca.
Existem quatro tipos de datas em arquivos de acordo com o stat
:
Para obter as últimas músicas adicionadas, e não as últimas editadas (às vezes
eu edito os metadados de uma música, e não quero que isso interfira nessa
lista), eu precisava usar a data de criação, mas ela não é suportada pelo MPD,
então é por isso que estou fadado a usar esse script,
gen_playlist_newest.sh
:
PLD=$HOME/.config/mpd/playlists find ~/ark/mus/genres -type f -regextype posix-extended -regex '.*\.(flac|mp3)' -exec stat --format '%W : %n' "{}" \; \ | sort -nr | head -n 100 | cut -d: -f2 | sed -e 's|.*mus/\(.*\)|\1|' > \ $PLD/1_Newest.m3u