Не знаю, как вам, но мне всегда было приятно, когда на экране плеера красуется обложка текущего альбома вместо унылой дефолтной картинки. Значительная часть релизов на всяких торрентах имеет оформленные теги вместе с картинками, однако, для остальной части прикреплять картинки вручную уж слишком накладно. Загуглить название группы и альбома, выбрать картинку приличного качества, скачать, а потом ещё назначать эту картинку через какой-нибудь проигрыватель. Бррр, сложно. Я не говорю уже о том, что все проигрыватели, через которые я это проделывал, уж как-то слишком медленно допиливают теги.
Взникла идея написать для этого скрипт: небольшое окошко где-нибудь в углу экрана. Перетаскиваешь туда файлы или папку с альбомом, а дальше программа сама ищет в интернете картинки и записывает их в mp3-файлы.
Ну и за одно хотелосьзанять себя чем-нибудь на унылых лекциях попрактиковаться в Python. И использовать это предполагается под Виндой, ибо недавно приобретённое 'апле' обязывает.
Под катом реализация данной идеи.
Откуда брать картинки, долго думать не пришлось: как следует из заголовка, я решил использовать last.fm. Мало того, что у них есть огромная база данных о музыке и исполнителях, там ещё, как оказалось, есть открытый API. Так что даже выдумывать ничего не пришлось.
Итак, взяв карандаш и бумагу, набросал алгоритм программы:
Да, прежде пара замечаний. Python начал изучать недавно, так что если если в коде что-то окажется не 'pythonic' — беспощадно забрасывайтепомидорами комментариями. Буду благодарен. Проверки и защиту от дураков делал по минимуму. Писал для себя, да и не люблю я оверинжиниринг.
Начинаю со второго пункта, поскольку графический интерфейс писал в последнюю очередь.
Итак, беглый поиск в интернете показал, что библиотек для работы с id3-тегами для Python не более десятка. Причём большинство из них либо созданы только для чтения тегов, либо не работают с картинками. По требованиям подошла
Итак, подключаем библиотеку:
Тут всё просто: передаём функции адрес mp3-файла и получаем в ответ кортеж из двух строк: названия исполнителя и альбома.
Да, предполагается, что соответствующие теги на месте (а они, как правило всегда присутствуют, а без них всё равно затея теряет смысл), поэтому все проверки я тут опустил.
Как я уже упомянул, у last.fm есть открытый API. Я даже честно зарегистрировался и получил свой ключ.
Интересующий нас метод —
Запрос будет выглядеть так:
Всё довольно просто. Стоит только помнить, что пробелы в названиях следует заменять плюсами. Функция тоже элементарная: используя
Хотя тут возник странный момент: этой функции предыдущая передаёт кортеж из двух строк, а приходит уже кортеж из двух списков по одному элементу в каждом:
Если кто-нибудь объяснит это, буду очень признателен. А пока пришлось выкручиваться:
Ответ от ластфм выглядит примерно так: ссылка.
Он содержит набор тегов
Взникла идея написать для этого скрипт: небольшое окошко где-нибудь в углу экрана. Перетаскиваешь туда файлы или папку с альбомом, а дальше программа сама ищет в интернете картинки и записывает их в mp3-файлы.
Ну и за одно хотелось
Под катом реализация данной идеи.
Откуда брать картинки, долго думать не пришлось: как следует из заголовка, я решил использовать last.fm. Мало того, что у них есть огромная база данных о музыке и исполнителях, там ещё, как оказалось, есть открытый API. Так что даже выдумывать ничего не пришлось.
Итак, взяв карандаш и бумагу, набросал алгоритм программы:
- Берём mp3-файл;
- Считываем из него название альбома и исполнителя;
- Формируем запрос для API last.fm;
- Обрабатываем XML-ответ last.fm и получаем адрес интересующей нас картинки;
- Скачиваем картинку;
- Записываем картинку в качестве соответствующего id3-фрейма в файл. Профит!
Да, прежде пара замечаний. Python начал изучать недавно, так что если если в коде что-то окажется не 'pythonic' — беспощадно забрасывайте
2. Чтение id3-тегов.
Начинаю со второго пункта, поскольку графический интерфейс писал в последнюю очередь.
Итак, беглый поиск в интернете показал, что библиотек для работы с id3-тегами для Python не более десятка. Причём большинство из них либо созданы только для чтения тегов, либо не работают с картинками. По требованиям подошла
eyeD3
и даже оказалась довольно удобной и интуитивно понятной, но вот картинки в файл записывать у меня упорно не получалось. Помучившись и так, и эдак, решил отправить её в утиль и найти замену. К счастью, замена нашлась, хоть и с очень странным и неочевидным названием — mutagen
. Это оказался довольно жирный пакет для работы с разными музыкальными форматами. Он содержит библиотеку easyid3
с простым и понятнымпространством имём, но проблему картинок он не решил. Поэтому я использовал более функциональный модуль id3. Однако, в отличие от easyid3
метки тегов там соответствуют не совсем очевидным именам из стандарта id3. Что ж, пришлось ознакомиться с этим стандартом, хотя, честно говоря, очень не хотелось.Итак, подключаем библиотеку:
from mutagen.mp3 import MP3<br/>
from mutagen import id3 as mu
и реализуем пункт 2.def fGetSongInfo(sSongLocation):<br/>
muTrack=MP3(sSongLocation)<br/>
sAlbum = muTrack.tags['TALB'].text<br/>
sArtist = muTrack.tags['TPE1'].text<br/>
return sArtist, sAlbum
Тут всё просто: передаём функции адрес mp3-файла и получаем в ответ кортеж из двух строк: названия исполнителя и альбома.
Да, предполагается, что соответствующие теги на месте (а они, как правило всегда присутствуют, а без них всё равно затея теряет смысл), поэтому все проверки я тут опустил.
3. Запрос для last.fm.
Как я уже упомянул, у last.fm есть открытый API. Я даже честно зарегистрировался и получил свой ключ.
Интересующий нас метод —
album.getInfo
.Запрос будет выглядеть так:
http:⁄⁄ws.audioscrobbler.com⁄2.0&⁄?
method=album.getinfo&api_key=API_ключ&artist=Имя_исполнителя
&album=Название_альбома
Всё довольно просто. Стоит только помнить, что пробелы в названиях следует заменять плюсами. Функция тоже элементарная: используя
urllib
, приводим названия в порядок, создаём словарик с названиями атрибутов и кодируем его в запрос.Хотя тут возник странный момент: этой функции предыдущая передаёт кортеж из двух строк, а приходит уже кортеж из двух списков по одному элементу в каждом:
print (lArtist, lAlbum)
([u'Chevelle'], [u'Wonder What's Next'])
Если кто-нибудь объяснит это, буду очень признателен. А пока пришлось выкручиваться:
def fCreateLfmRequest((lArtist, lAlbum)):<br/>
'''<br/>
Returns Last.FM API request<br/>
'''<br/>
print (lArtist, lAlbum)<br/>
sArtist=''.join(lArtist).encode('cp1251')<br/>
sAlbum=''.join(lAlbum).encode('cp1251')<br/>
<br/>
sArtistTmp=ul.quote_plus(sArtist)<br/>
sAlbumTmp=ul.quote_plus(sAlbum)<br/>
<br/>
dParams={"api_key" : sApiKey, 'artist' : sArtistTmp, 'album' : sAlbumTmp, 'method' : 'album.getinfo'}<br/>
return ul.unquote(ul.urlencode(dParams))<br/>
4. Парсинг ответа.
Ответ от ластфм выглядит примерно так: ссылка.
Он содержит набор тегов
с адресами сгенерированных автоматически картинок размерами от 34*34 до 300*300, а также исходник, который, не смотря на свё название, может быть и меньше 300*300. Выбираем размер по вкусу. Меня лично интересовал 300*300, ибо разрешение экрана на плеере 240*240.
Незадолго до реализации моей задумки на Хабре пиарили BeautifulSoup
, его я и использовал для парсинга.from BeautifulSoup import BeautifulStoneSoup as bs <br/>
Это тоже реализуется в пару строчек, не считая проверки. Передаём исходник и название метки, получаем URL картинки.def fUnparseImgUrl(sSrc, sSizeId):<br/>
'''<br/>
Gets an image URL from Last.FM querry result; returns URL of image ("large", 300x300).<br/>
'''<br/>
bsSoup=bs(sSrc)<br/>
if bsSoup.find('image', size=sSizeId)==None: return ERROR_MSG<br/>
else:<br/>
try:<br/>
return bsSoup.find('image', size=sSizeId).text<br/>
except:<br/>
return ERROR_MSG<br/>
5. Скачиваем картинку.
Берём и скачиваем. Я сохраняю как "Folder.jpg" в папке с альбомом и потом не удаляю. Авось пригодится ещё каком-нибудь приложению.def fDownload(sSource, sTarget):<br/>
'''<br/>
Downloads file from Internet.<br/>
sSource - source file URL<br/>
sTarget - target file name<br/>
'''<br/>
#Если альбом не нашёлся, качаем что-нибудь другое.<br/>
if sSource == ERROR_MSG: sSource='http://cdn.last.fm/depth/global/download_aslogo_bluebg.jpg'<br/>
<br/>
fSourceImg = ul.urlopen(sSource)<br/>
fTargetImg = open(sTarget, "wb")<br/>
sData = fSourceImg.read()<br/>
fTargetImg.write(sData)<br/>
fTargetImg.close()<br/>
fSourceImg.close()
В случае, если на last.fm по каким-то причинам ничего не нашлось, скачиваем другую картинку. Вместо этого я планировал написать функцию, которая генерит какую-нибудь картинку с названием исполнителя и альбома, но руки пока не дошли, да и не в тему статьи это. Поэтому заменил на такую затычку.
6. Прикрепляем картинку.
Тег для картинки выглядит так: 'APIC:метка_картинки'
. Таким образом, картинок можно добавлять сколько угодно, лишь бы ключ начинался с "APIC
" и двоеточия.
Значением по этому ключу является экземпляр класса APIC
, имеющего аттрибуты encoding, mime, type, desc, data
. Их назначение описано здесь.
Делаем простенькую проверку типа по расширению, создаём тег, добавляем к исходному файлу и не забываем сохранить. Аргумент v1 метода save() имеет следующий смысл:
if 0, ID3v1 tags will be removed
if 1, ID3v1 tags will be updated but not added
if 2, ID3v1 tags will be created and/or updated,
Так что выбираем двойку.
def fBindArtwork(sTrackAddr, sImgAddr, sArtAppend=u'frontcover'):<br/>
'''<br/>
Writes image data to mp3 file as APIC id3 frame.<br/>
sTrackAddr - audio file address;<br/>
sImgAddr - PNG/JPEG image file address;<br/>
'''<br/>
#Open an Mp3 file<br/>
muTrack=MP3(sTrackAddr)<br/>
#Open picture file<br/>
fPicture=file(sImgAddr, 'rb')<br/>
sPic=fPicture.read()<br/>
<br/>
#Cheching image format<br/>
if op.splitext(sImgAddr)[-1].lower()=='png': sMimeType=MIME_PNG<br/>
elif (op.splitext(sImgAddr)[-1].lower()=='jpg'<br/>
or op.splitext(sImgAddr)[-1].lower()=='jpeg'<br/>
or op.splitext(sImgAddr)[-1].lower()=='jpe'): sMimeType=MIME_JPEG<br/>
else: sMimeType=''<br/>
<br/>
#Creating a temporary APIC instance<br/>
sArtworkTag=ARTWORK_TAG+sArtAppend<br/>
muPic=mu.APIC(encoding=3, mime=MIME_JPEG, type=FRONT_COVER,<br/>
desc=u'Album front cover', data=0)<br/>
muTrack.tags[sArtworkTag]=muPic<br/>
<br/>
#Write image data to mp3 file<br/>
muTrack.tags[sArtworkTag].data=sPic<br/>
muTrack.save(v1=2)<br/>
<br/>
fPicture.close()
1. Графический интерфейс.
Подробно описывать не буду, ибо это не самая важная часть скрипта. Тут я использовал wxPython
.
cDropFrame
- производный от wx.Frame
класс, собственно само окошко.
CDropTarget
- класс для реальзиции драг-н-дропа. В методе OnDropFiles
описывается его поведение.
Я не хотел лазить по деревьям папок (ибо себе не доверяю), потому сделал обработку только файлов внутри папки, если скинута папка, ну либо просто файлов, если перетащили файлы. А точнее, и то, и другое.
fProcessTracks
- функция реализует написанный в начале статьи алгоритм. Проверок там городить не стал, ибо не вижу необходимости.
Предполагается, что внутри папки или среди скинутых файлов содержатся треки из одного и того же альбома (лично я бардака в своей музыкальной коллекции не допускаю), так что обложка скачивается один раз и назначается всем файлам.
Как это выглядит
А выглядит это примерно так:

Процесс назначения картинок для среднего альбома занимает не более трёх-пяти секунд. Я даже сперва не поверил и полез проверять файлы в айтюнсах. Ещё меня удивило, что при всей небрежности моих тестов треки не повреждались и после пыток исправно воспроизводились и на компе, и даже в капризном плеере.
Единственная проблема, которую я не смог решить — это работа с русскоязычными тегами. Похоже, что API last.fm просто с ними не работает. Печально, однако.
Код целиком.
GUI.pyw
import wx<br/>
import os.path as op<br/>
import os<br/>
import pyBinder as pb<br/>
<br/>
FRAME_OPACITY=200<br/>
DEFAULT_IMAGE_NAME='Folder.jpg'<br/>
<br/>
def fProcessTracks(lFileNames, wFrame):<br/>
'''Обработка списка файлов'''<br/>
<br/>
#Ищем в списке первый mp3-файл...<br/>
for sSongLocation in lFileNames:<br/>
if op.splitext(sSongLocation)[-1].lower()=='.mp3': break<br/>
<br/>
#...и получаем для него обложку альбома<br/>
wFrame.fSetGauge('Getting song info...', 20)<br/>
tSongInfo= pb.fGetSongInfo(sSongLocation)<br/>
<br/>
sImageLocation=op.join(op.dirname(sSongLocation), DEFAULT_IMAGE_NAME)<br/>
<br/>
wFrame.fSetGauge('Creating Last.FM request...', 40)<br/>
sLfmQuery=pb.fCreateLfmRequest(tSongInfo)<br/>
<br/>
wFrame.fSetGauge('Getting data from Last.FM...', 60)<br/>
sLfmResult=pb.fQueryLfm(sLfmQuery)<br/>
<br/>
wFrame.fSetGauge('Analysing Last.FM request...', 80)<br/>
sImgUrl=pb.fUnparseImgUrl(sLfmResult, pb.SIZE_300X300)<br/>
<br/>
wFrame.fSetGauge('Downloading image...', 90)<br/>
pb.fDownload(sImgUrl, sImageLocation)<br/>
<br/>
wFrame.fSetGauge('Almost ready.', 100)<br/>
wFrame.fSetImage(sImageLocation)<br/>
<br/>
#Назначаем всем mp3-файлам в списке полученную картинку<br/>
#(Предполагается, что все треки из одного альбома)<br/>
nGaugeStep=int(100/len(lFileNames))<br/>
nGaugeValue=0<br/>
for sFileName in lFileNames:<br/>
if op.isfile(sFileName) and (op.splitext(sFileName)[-1].lower() == '.mp3'):<br/>
nGaugeValue+=nGaugeStep<br/>
wFrame.fSetGauge(op.basename(sFileName), nGaugeValue)<br/>
pb.fBindArtwork(sFileName, sImageLocation)<br/>
#Готово.<br/>
wFrame.fSetGauge('Ready', 100)<br/>
<br/>
class CDropTarget(wx.FileDropTarget):<br/>
'''Класс для Drag and Drop'''<br/>
<br/>
def __init__(self, wTargetWidget):<br/>
wx.FileDropTarget.__init__(self)<br/>
self.window=wTargetWidget<br/>
self.wFrame=self.window.GetParent()<br/>
<br/>
def OnDropFiles(self, x, y, lDroppedContent):<br/>
#Немного индусской магии!<br/>
lFileNames=[]<br/>
for sPath in lDroppedContent:<br/>
if op.isdir(sPath):<br/>
lInnerFileNames=[op.join(sPath, sFileName) for sFileName in os.listdir(sPath)]<br/>
fProcessTracks(lInnerFileNames, self.wFrame)<br/>
if op.isfile(sPath):<br/>
lFileNames.append(sPath)<br/>
if lFileNames!=[]:<br/>
fProcessTracks(lFileNames, self.wFrame)<br/>
<br/>
class CDropFrame(wx.Frame):<br/>
'''Окошко приложения'''<br/>
<br/>
def __init__(self, nSize):<br/>
wx.Frame.__init__(self, None, title='pyBinder',<br/>
style=wx.STAY_ON_TOP|wx.FRAME_TOOL_WINDOW|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU)<br/>
<br/>
#Под Виндой size - это размер окна вместе с заголовком,<br/>
#поэтому для создания квадратного окошко задаём clientSize вместо size<br/>
self.SetClientSize((nSize,)*2)<br/>
self.nSize=nSize<br/>
<br/>
#wPanel принимает Drag'нутые объекты<br/>
self.wPanel=wx.Panel(self,pos=(0,0), size=(nSize,)*2)<br/>
self.wPanel.SetBackgroundColour('#FFFFFF')<br/>
wDropTarget=CDropTarget(self.wPanel)<br/>
self.wPanel.SetDropTarget(wDropTarget)<br/>
<br/>
#Картинка для красоты.<br/>
#Позже заменяется на обложку альбома функцией fProcessTracks()<br/>
self.wBgFrame=wx.StaticBitmap(self.wPanel)<br/>
self.fSetImage('Backgrounds\\Background2.jpg')<br/>
<br/>
#Текстовое поле ProgressBar<br/>
self.wText=wx.StaticText(self.wBgFrame, -1, 'Drop some mp3 files here...', (0,(nSize/2)-15), (nSize,35), wx.ALIGN_CENTER)<br/>
self.wText.SetBackgroundColour('#000000')<br/>
self.wText.SetForegroundColour('#FFFFFF')<br/>
<br/>
self.wGauge=wx.Gauge(self.wText, -1, 100, (5,20), (nSize-10,10))<br/>
self.wGauge.SetValue(0)<br/>
<br/>
#Разполагаем окошко в правом нижнем углу экрана<br/>
tScreenSize=wx.ScreenDC().GetSize()<br/>
tFrameSize=self.GetSize()<br/>
self.SetPosition(tuple(tScreenSize-tFrameSize))<br/>
<br/>
#Делаем прозрачность для понта<br/>
self.SetTransparent(FRAME_OPACITY)<br/>
#Готово!<br/>
self.Show()<br/>
<br/>
#Установка фонового изображения<br/>
def fSetImage(self, sImgSrc):<br/>
wImage=wx.Image(sImgSrc)<br/>
wImage.Rescale(self.nSize, self.nSize)<br/>
wImage=wImage.ConvertToBitmap()<br/>
self.wBgFrame.SetBitmap(wImage)<br/>
#Обновление текста и статус-бара<br/>
def fSetGauge(self, sMessage, nValue):<br/>
self.wText.SetLabel(sMessage)<br/>
#Размер текстового поля приходится каждый раз обновлять =\<br/>
self.wText.SetSize((self.nSize,35))<br/>
self.wGauge.SetValue(nValue)<br/>
<br/>
if __name__=='__main__':<br/>
aApp=wx.PySimpleApp()<br/>
wDropFrame=CDropFrame(180)<br/>
aApp.MainLoop()
pyBinder.py
import urllib as ul<br/>
import httplib as hl<br/>
from BeautifulSoup import BeautifulStoneSoup as bs<br/>
from mutagen.mp3 import MP3<br/>
from mutagen import id3 as mu<br/>
<br/>
FRONT_COVER = 3<br/>
<br/>
ARTWORK_TAG = u'APIC:'<br/>
MIME_PNG = 'image/png'<br/>
MIME_JPEG = 'image/jpeg'<br/>
<br/>
SIZE_34X34=b'small'<br/>
SIZE_64X64=b'medium'<br/>
SIZE_174X174=b'large'<br/>
SIZE_300X300=b'extralarge'<br/>
SIZE_RAW=b'mega'<br/>
<br/>
ERROR_MSG=b'FAYUUUL!'<br/>
<br/>
sApiKey = b'c8e1cd060b177d97acd579faa5a4ef5e'<br/>
sLfmUrl='ws.audioscrobbler.com'<br/>
<br/>
def fDownload(sSource, sTarget):<br/>
'''<br/>
Downloads file from Internet.<br/>
sSource - source file URL<br/>
sTarget - target file name<br/>
'''<br/>
#Если альбом не нашёлся, качаем что-нибудь другое.<br/>
if sSource == ERROR_MSG: sSource='http://cdn.last.fm/depth/global/download_aslogo_bluebg.jpg'<br/>
<br/>
fSourceImg = ul.urlopen(sSource)<br/>
fTargetImg = open(sTarget, "wb")<br/>
sData = fSourceImg.read()<br/>
fTargetImg.write(sData)<br/>
fTargetImg.close()<br/>
fSourceImg.close()<br/>
<br/>
def fBindArtwork(sTrackAddr, sImgAddr, sArtAppend=u'frontcover'):<br/>
'''<br/>
Writes image data to mp3 file as APIC id3 frame.<br/>
sTrackAddr - audio file address;<br/>
sImgAddr - PNG/JPEG image file address;<br/>
'''<br/>
#Open an Mp3 file<br/>
muTrack=MP3(sTrackAddr)<br/>
#Open picture file<br/>
fPicture=file(sImgAddr, 'rb')<br/>
sPic=fPicture.read()<br/>
<br/>
#Cheching image format<br/>
if op.splitext(sImgAddr)[-1].lower()=='png': sMimeType=MIME_PNG<br/>
elif (op.splitext(sImgAddr)[-1].lower()=='jpg'<br/>
or op.splitext(sImgAddr)[-1].lower()=='jpeg'<br/>
or op.splitext(sImgAddr)[-1].lower()=='jpe'): sMimeType=MIME_JPEG<br/>
else: sMimeType=''<br/>
<br/>
#Creating a temporary APIC instance<br/>
sArtworkTag=ARTWORK_TAG+sArtAppend<br/>
muPic=mu.APIC(encoding=3, mime=MIME_JPEG, type=FRONT_COVER,<br/>
desc=u'Album front cover', data=0)<br/>
muTrack.tags[sArtworkTag]=muPic<br/>
<br/>
#Write image data to mp3 file<br/>
muTrack.tags[sArtworkTag].data=sPic<br/>
muTrack.save(v1=2)<br/>
<br/>
fPicture.close()<br/>
<br/>
def fGetSongInfo(sSongLocation):<br/>
'''Gets Artist and Album data from an mp3 file.<br/>
Returns a string (Artist, Album) tuple<br/>
sSongLocation - location of the file'''<br/>
muTrack=MP3(sSongLocation)<br/>
sAlbum = muTrack.tags['TALB'].text<br/>
sArtist = muTrack.tags['TPE1'].text<br/>
return sArtist, sAlbum<br/>
<br/>
def fCreateLfmRequest((lArtist, lAlbum)):<br/>
'''<br/>
Returns Last.FM API request<br/>
'''<br/>
print (lArtist, lAlbum)<br/>
sArtist=''.join(lArtist).encode('cp1251')<br/>
sAlbum=''.join(lAlbum).encode('cp1251')<br/>
<br/>
sArtistTmp=ul.quote_plus(sArtist)<br/>
sAlbumTmp=ul.quote_plus(sAlbum)<br/>
<br/>
dParams={"api_key" : sApiKey, 'artist' : sArtistTmp, 'album' : sAlbumTmp, 'method' : 'album.getinfo'}<br/>
return ul.unquote(ul.urlencode(dParams))<br/>
<br/>
def fQueryLfm(sLfmRequest):<br/>
'''<br/>
Returns an XML result from Last.FM<br/>
'''<br/>
cCon=hl.HTTPConnection(sLfmUrl)<br/>
<br/>
cCon.request('GET', '/2.0/?'+sLfmRequest) #Здесь я слегка схалтурил =)<br/>
cResult = cCon.getresponse()<br/>
return cResult.read()<br/>
cCon.close()<br/>
<br/>
def fUnparseImgUrl(sSrc, sSizeId):<br/>
'''<br/>
Gets an image URL from Last.FM querry result; returns URL of image ("large", 300x300).<br/>
'''<br/>
bsSoup=bs(sSrc)<br/>
if bsSoup.find('image', size=sSizeId)==None: return ERROR_MSG<br/>
else:<br/>
try:<br/>
return bsSoup.find('image', size=sSizeId).text<br/>
except:<br/>
return ERROR_MSG