четверг, 17 января 2013 г.

Python как автоматизатор рутины: музыка

Есть еще один процесс, который занимает мое время. Это отслеживание музыкальных новинок. Частично с этим помогает LastFm. Но попользовавшись им 2 года осознал недостатки:
  • уведомление приходит достаточно поздно (вплоть до 2-3 месяцев) с момента выхода нового альбома. Может совсем не прийти
  • приходит всякий шлак (синглы, компиляции и т.п.)  - т.е. не новый материал
Поэтому приходилось периодически пробегаться по списку исполнителей и гуглить их творческие успехи за последний период. Если находил что-то, то шел в магазин за диском. Да-да, все туда же - сначала за торрентами.

Почему бы не автоматизировать и этот процесс? Подписываемся (как и в кино) на раздачи в интересующих жанрах (и, конечно, в loseless, т.к. mp3 это моветон), анализируем раздачи используя информацию из LastFm, скачиваем, profit! Но не так всё просто как с фильмами. В музыкальной индустрии на один действительно новый релиз приходится несколько перекомпиляций, ремастерингов, синглов, переизданий, original press и прочего. То есть полностью автоматизировать скачку означает достаточно быстро забить музыкальную библиотеку дубликатами в которых потом не разобраться. Нужен человеческий фильтр. 

Как его сделать? За обедом обсуждал это с коллегами где они подкинули идею использовать twitter. Что-то вроде - заретвитил сообщение о раздаче - значит её действительно нужно скачать. Подписываешься на робота и читаешь его публикации - если есть стоящее - ретвитишь, и робот раздачу скачает.

Итак, вырисовалась следующая последовательность:
  1. Получаем новые раздачи в нужных жанрах (да-да, я снова о ру-трекере)
  2. Загружаем исполнителей и уже прослушанные альбомы из LastFm
  3. Если раздача нужного исполнителя и такой альбом еще не существует в базе - публиковать в твиттер
  4. Если сообщение ре-твитится нужным пользователем - ставить на закачку

feedparser никуда не изчез - используем его.
Обертки LastFm API для Python я нашел 2: pylast и lastfmapi. Первый оказался достаточно мощным (поддерживает скробблинг), но, к сожалению использует не все параметры вызовов - полагается на умолчания. Я смог это обойти отредактировав исходники, но потом решил что это неприемлемо и переключился на второй - lastfmapi. Он более простой (кажется автор написал всего пару месяцев назад) но, как следствие, мощный.
В процессе работы выяснилась интересная особенность LastFm. Если у артиста несколько имен (например Земфира и Zемфира) - то в списке артистов вернется только "правильная" версия имени. В списке же альбомов имя артиста может быть и другим. Пока что я отказался от умного анализа таких совпадений и просто складываю оба варианта себе в фильтр.

Так как раздачи менее формальны, чем в фильмах, парсить имя артиста и имя альбома сложнее и примерно у 5% раздач имя альбома парсится неправильно. Это некритично для ненужных исполнителей, а у нужных я лучше лишний раз глазами просмотрю.

Идею с ретвитом я передумал, так как твиттером не пользуюсь а интересные мне микроблоги читаю как всегда. Зато в Google Reader (пока еще) есть замечательная кнопка "share" - которая rss-пост копирует в мой собственный rss-поток (его вы можете наблюдать в колонке слева этого блога). Так как я всё читаю через Google Reader и его айфонный аватар - MobileRSS - это неплохой способ. Подписываюсь на собственного робота в Google Reader и читаю его твиты. Если есть стоящая музыкальная раздача - жму кнопку "share" и робот эту раздачу скачает.

Итак, завершающий шаг оказался донельзя простым. Робот подписывается на мой rss поток из пошареных постов и если обнаруживает там собственный твит - ставит его на закачку. Здесь пришлось продумать айдишники раздачи и прикрутить маркер уже скачанных раздач.

Результат все там же


1    __author__ = 'Smog' 
2     
3    #standard packages 
4    import re 
5     
6    #additional packages 
7    import feedparser, lastfmapi 
8     
9    environment = { 
10       'lastfm_username' : "", 
11       'lastfm_key': "", 
12       'lastfm_secret': "" 
13   } 
14    
15   #------------------------- TORRENT ALBUM ------------------------------------------------------------------------------- 
16    
17   class TorrentAlbum: 
18       def __init__(self, rssEntry): 
19           self.topicURL = rssEntry.link 
20           self.origTitle = rssEntry.title 
21           self.topic = self.topicURL[len('http://rutracker.org/forum/viewtopic.php?'):] 
22    
23           m = re.search('(\) [^\-]+\-)', rssEntry.title) 
24           #        print rssEntry.title 
25           if (m != None): 
26               self.artist = m.group(0)[2:-1].strip() 
27               start = m.regs[0][1] 
28               #            print rssEntry.title[start:] 
29    
30               m = re.search('(.)+\d\d\d\d', rssEntry.title[start:]) 
31    
32               if ( m != None): 
33                   self.title = m.group(0)[:-4] 
34                   rightB = len(self.title) 
35                   for i in range(len(self.title)): 
36                       sym = self.title[-1 * i] 
37                       if (sym.isalpha() or sym.isdigit()): 
38                           rightB = -1 * i 
39                           break 
40    
41                   self.title = self.title[:rightB + 1].strip() 
42               else: 
43                   print 'NO ALBUM: ' + rssEntry.title 
44                   self.title = 'None' 
45    
46           else: 
47               print 'NO ARTIST: ' + rssEntry.title 
48               self.artist = 'None' 
49               self.title = 'None' 
50    
51           print self.artist + '\t' + self.title + '\t' + self.topicURL 
52    
53       def getTweet(self, bitly): 
54           torrentLink = bitly.shorten(longUrl=self.topicURL)['url'] 
55           tail = " " + torrentLink + " smbid=" + self.topic + " #music" 
56           return self.origTitle[:140 - len(tail)] + tail 
57    
58       def __str__(self): 
59           result = map(lambda x: x + "=" + unicode(self.__dict__.get(x)), self.__dict__.keys()) 
60           result.sort() 
61           return result.__str__() 
62    
63   #------------------------- LAST FM AGENT ------------------------------------------------------------------------------- 
64    
65   class LastFmAgent: 
66       def __init__(self): 
67           api = lastfmapi.LastFmApi(environment.get('lastfm_key')) 
68    
69           artists = [] 
70           totalPages = 1 
71           page = 1 
72           print 'Loading artists...' 
73    
74           while (page <= totalPages): 
75               jsonArtists = api.user_getTopArtists(user=environment.get('lastfm_username'), period='overall', limit=100, page=page) 
76               totalPages = int(jsonArtists['topartists']['@attr']['totalPages']) 
77               page = int(jsonArtists['topartists']['@attr']['page']) + 1 
78    
79               weightedArtists = filter(lambda a: int(a['playcount']) > 15, jsonArtists['topartists']['artist']) 
80               artists.extend(map(lambda a: a['name'].lower(), weightedArtists)) 
81               print 'Page ' + str(page) + ' / ' + str(totalPages) 
82    
83           print 'Found ' + str(len(artists)) + ' LastFm artists' 
84           self.artistAlbums = {} 
85           for a in artists: 
86               self.artistAlbums[a] = [] 
87    
88           totalPages = 1 
89           page = 1 
90           print 'Loading albums...' 
91    
92           while (page <= totalPages): 
93               jsonAlbums = api.user_getTopAlbums(user=environment.get('lastfm_username'), period='overall', limit=100, page=page) 
94               totalPages = int(jsonAlbums['topalbums']['@attr']['totalPages']) 
95               page = int(jsonAlbums['topalbums']['@attr']['page']) + 1 
96    
97               albums = filter(lambda a: a['playcount'] > 10, jsonAlbums['topalbums']['album']) 
98    
99               for a in albums: 
100                  if (self.artistAlbums.has_key(a['artist']['name'].lower())): 
101                      self.artistAlbums[a['artist']['name'].lower()].append(a['name'].lower()) 
102                  else: 
103                      self.artistAlbums[a['artist']['name'].lower()] = [a['name'].lower()] 
104   
105              print 'Page ' + str(page) + ' / ' + str(totalPages) 
106   
107          print 'Found ' + str(len(self.artistAlbums.values())) + ' LastFm albums' 
108   
109   
110      def isWorthPublishing(self, album): 
111          if not (self.artistAlbums.has_key(album.artist.lower())): 
112              return False 
113   
114          return self.artistAlbums.get(album.artist.lower()).count(album.title.lower()) == 0 
115   
116  #------------------------- MUSIC ANALYSER MAIN ------------------------------------------------------------------------- 
117   
118  def checkAndPublishMusic(): 
119      feeds = [ 
120          'http://feed.rutracker.org/atom/f/737.atom', #http://rutracker.org/forum/viewforum.php?f=737&start=100 
121          'http://feed.rutracker.org/atom/f/1702.atom', 
122          'http://feed.rutracker.org/atom/f/739.atom', 
123          'http://feed.rutracker.org/atom/f/1706.atom', 
124          'http://feed.rutracker.org/atom/f/1704.atom', 
125          'http://feed.rutracker.org/atom/f/1708.atom', 
126          'http://feed.rutracker.org/atom/f/1726.atom', 
127          'http://feed.rutracker.org/atom/f/1724.atom', 
128          'http://feed.rutracker.org/atom/f/1744.atom', 
129          'http://feed.rutracker.org/atom/f/1748.atom', 
130          'http://feed.rutracker.org/atom/f/1742.atom', 
131          'http://feed.rutracker.org/atom/f/1857.atom' 
132      ] 
133   
134      print 'Getting LastFM statistics...' 
135      lfmAgent = LastFmAgent() 
136   
137      print '\nSigning in to Twitter...' 
138      tw = TwitterAgent() 
139      processedAlbums = tw.getProcessedItemsIds() 
140   
141      print '\nLoading feeds...' 
142      for feed in feeds: 
143          f = feedparser.parse(feed) 
144          print "\n\nLoaded " + f.feed.title + "\n" 
145   
146          albums = map(lambda x: TorrentAlbum(x), f.entries) 
147          albums = filter(lambda  x: lfmAgent.isWorthPublishing(x) and x.topic not in processedAlbums, albums) 
148   
149          if len(albums) > 0: 
150              for a in albums: 
151                  print 'RECOMMENDED TO DOWNLOAD ' + a.origTitle 
152                  tw.tweet(a.getTweet(tw.bitly)) 
153   
154   
155  checkAndPublishMusic() 
156  


Дальнейшее развитие идеи
  1. Конвертировать Image+CUE раздачи в треки (по-файлам);
  2. Создавать mp3 версию раздачи
  3. Загружать ее в iTunes
  4. Добавлять в iTunes плейлист (?)
пока застопорилось на первом шаге, так как мой (не)любимый foobar2000 из командной строки оказался неспособен сконвертировать созданный плейлист. Буду искать консольный конвертер.

Комментариев нет:

Отправить комментарий