13 июл. 2009 г.

id3project is alive!

В течение последней недели активно прогаю свой замечательный id3project.
Снова вернулось вдохновение - то есть интерес. я заметил, что мне хочется прогать исключительно тогда, когда интересно,, интересно, когда прогаешь что-нибудь содержательное. но я ничего не заканчиваю (в большинстве случаев), потому что в любой задаче есть тривиальная часть - которая не менее важна для конечного результата, но не так интересна,, а когда мне неинтересно ничего не хочется делать. это очень плохо. надо делать и то и другое,, потому что иначе половина работы не имеет смысла и оказывается лишь бесполезным осадком от удовольствия её воплощения.

но о чём это я?.. об id3project!
за эту неделю я многое сделал. хотя вспомню я сейчас не всё и не в хронологическом порядке.
помимо изменений в самой библиотеке, о которых речь пойдёт дальше, я начал использовать darcs - хаскеллевскую систему контроля версий. то есть он у меня и раньше был. но теперь я попробовал подружиться с ним и действительно понять зачем мне это надо и как что-нибудь с его помощью сделать. удобно, наверное..
а ещё я нашёл бесплатных хостинг для проектов использующих darcs на patch-tag.com
не знаю что в нём ещё хорошего, кроме того, что он бесплатный и для darcs.. но мне-то и этого хватит сполна.
итак, дело приняло серьёзный оборот! думаю, скоро можно будет уже выкладывать библиотеку в hackage.

теперь, о самой библиотеке:
- были сделаны значительные усовершенствования в структурах данных, хранящих информацию тега. не упоминая конкретики самой структуры, сложно рассказать о каких-то изменениях в общем. упомяну лишь, что я стал использовать (впрочем давно) такую штуку как аксессоры из пакета data-accessor (не знаю как это можно разумно перевести). все структуры данных, которые я использую - записи (records). у записей есть поля содержащие информацию. так вот стандартный интерфейс доступа к этим полям в хаскелле довольно неудобный (имхо). аксессоры предлагают (1) более удобный интерфейс, (2) абстракцию, которая позволяет менять эти методы доступа к полям записи. так что когда я менял структуры, мне не приходилось исправлять код везде, где они используются - я просто менял аксессоры.
- функция чтения тега из файла стала лучше, проще, надёжнее.
- функция записи тега в файл была наконец таки сделана! и если я чего-нибудь не упустил,, работает она не только правильно, но и эффективно. в чём была сложность: тег хранится вначале музыкального файла. у тега есть паддинг - резервное место для дописывания (кусок файла забитый нулями). это нужно для того, чтобы если тег увеличится, не переписывать весь файл заново (у меня например встречаются mp3 по 30-40 мегабайт), а записать его поверх старого, уменьшив тем самым паддинг (пэддинг). я разобрался как это сделать аккуратно и вроде бы у меня получилось.
- и наконец, самое(?) главное! я сделал "простой интерфейс" для этой библиотеки. простой интерфейс в кавычках, потому что это - во-первых (мягко говоря) урезанная функциональность, во-вторых это называется интерфейсом в смысле программирования, а не пользования. то есть интерфейс для тех, кто программирует на хаскелле. он действительно простой. но если это то, ради чего я делал всё что я делал (а это разумеется не так),, то я был настоящим извращенцем - это как строить большой, очень надёжный самолёт, с хорошими аэродинамическими характеристиками, в котором для пилота предусмотрена удобная приборная панель с бесчётным числом рычажков, кнопочек, тумблеров и прочих элементов управления,, а потом сделать для него пульт дистанционного управления (это ведь удобно) с двумя кнопками (всё просто): "/ехать/ вперёд" и "/ехать/ назад" (ничего лишнего).
вот примерно также и с этим моим простым интерфейсом..
там есть несколько функций такого вида:
getArtist :: Tag → Maybe String
она берёт тэг и возвращает либо Nothing - ежели информации об исполнителе в тэге не имеется, либо (Just "имярек") - ежели исполнитель имярек.

setArtist :: String → Tag → Tag
эта функция берёт имя которое вы хотите записать, тэг и возвращает изменённый тэг. при этом если информация об исполнителе в тэге имелась - она будет изменена,, а если е\ не было - будет создана.

в модуле ещё несколько точно таких же функций для названия трека, номера трека, года записи и названия альбома - в ближайшее время добавлю ещё парочку (комментарий, жанр и т.п.). всё очень просто - даже не надо знать что такое Tag. кстати, откуда он тогда берётся? из файла очевидно,, с помощью функции readTag:
readTag :: FilePath → IO (Maybe Tag)
она берёт имя файла, а возвращает "может быть тэг". это значит, что если в данном файле есть id3-тэг - значение будет иметь вид (Just tag),, а если нет - Nothing.

ну и для полноты картины надо упомянуть обратную функцию - для записи тега в файл:
writeTag :: FilePath -> Tag -> IO ()
куда писать и что писать - всё просто.

итак,, стоит написать маленький пример, чтобы показать как это всё работает и о чём вообще речь.
суть нижеследующего листинга в следующем: мы прочитаем тэг, посмотрим, есть ли в нём информация о номере трека, и если есть, отредактируем - если это цифра, то поставим перед ней ноль. по-моему вполне разумное изменение - некоторые плееры некорректно сортируют треки по номерам, если не проставлены нули слева:
1, 10, 11, 2, 3 .. - это происходит потому, что номер трека хранится в теге ввиде строки, а не числа и в данном случае эти строки сортируются в лексикографическом порядке - по алфавиту.
а мы сделаем так: 01, 02, 03 .. 10, 11.


 1 import System       (getArgs)
 2 import ID3.Simple
 3
 4 main = do
 5     files ← getArgs                         -- аргументы программы - список файлов, для редактирования
 6     mapM_ (withTag correctTrack) files      -- применим к каждому преобразование
 7
 8 -- в этой функции мы изолируем ввод-ввывод    (надо бы мне добавить её в библиотеку)
 9 withTag :: (Tag → Tag) → FilePath → IO ()
10 withTag function file = do
11     smth ← readTag file                            -- читаем "может быть тэг" из файла
12     case smth of
13          Nothing  → return ()                      -- если тэга в файле нет, то не делаем ничего
14          Just tag → writeTag file (function tag)   -- если тэг есть, преобразуем его и запишем обратно в файл
15
16 -- теперь, собственно, само преобразование
17 correctTrack :: Tag → Tag
18 correctTrack tag =
19     case getTrack tag of                 -- смотрим номер трека в тэге
20          Nothing  → tag                  -- если его там нет, то возвращаем тэг без изменений
21          Just num → if length num ≡ 1    -- а если есть - смотрим цифра ли это (один символ в строке)
22                        then setTrack ('0':num) tag     -- если да - добавляем слева ноль и пишем в тэг
23                        else tag                        -- если нет - ничего не меняем

можно было бы написать всё короче,, но я постарался сделать пример наглядным.
кроме того,, несомненное достоинство данного кода заключается в том, что он рабочий. я только что проверил. даже не понадобилось ничего исправлять - просто запустил и всё работает.

стоит ещё сказать о том, как проверить, что вообще что-то произошло - как посмотреть сам тэг. очень просто: print tag.
поскольку я сделал все структуры данных используемые в библиотеке членами класса Show, можно просто выводить структуру на экран без дополнительной обработки. при этом информация будет вполне читабельно форматирована.

пост получился и так уж слишком большой.. так что о том, что ещё собираюсь сделать напишу в следующем.

p.s.
перечитал и заметил, что пишу то "тэг", то "тег".. а как правильно?