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

SSD в ноутбук

У меня вообще сейчас 2 ПК: HTPC, про который я уже писал, и ноутбук, про который еще нет.
Ноутбук по нынешним меркам старенький, купленный в далеком 2007 году. С момента покупки служил мне верой и правдой и пережил три основных события: апгрейд памяти до 2Гб (это его технологический максимум), замену видеочипа (полгода в ремонте прохлаждался) и установку SSD как основного "жесткого" диска. Про последнее я и хочу рассказать. Апгрейд я делал осенью 2011 года, поэтому это скорее историческая хроника.

Для работы операционки, браузера, антивируса и прочего офиса достаточно часто нужен именно быстрый винт, а не что-либо еще. Быстрый винт на сегодня - это SSD. Так как в самом ноуте реализована SATA II, то совсем быстрые / новомодные SSD на SATA III+ не годились - ноут просто не смог бы реализовать их потенциал. А вот на SATA II вполне. Размер мне тоже был нужен не очень большой - так как ноут 17'' и в нем есть второй отсек для жесткого диска. Вот они оба (главный справа, вторичный слева):


В выборе модели я остановился на OCZ Vertex 2 на 80 Гб:

Старый винт хотелось сохранить для работы с данными (120 Гб все-таки). Но чтобы подключить его ко второму отсеку ноута, нужен был переходник с обычной SATA на ноутбучную плату. Вот так он выглядит:


Его я купил на интернет-барахолке. От родного он не отличался почти ничем:


Два отсека в ноуте идентичны по интерфейсу, но разные в физическом смысле. Во первых, первый отсек имеет вентиляционные отверстия, а во вторых - операционка всегда пытается грузиться именно с него. На второй даже не смотрит.



Поэтому оставить родной винт в первом отсеке, а SSD воткнуть во второй и поставить на него операционку у меня не получилось. Так что винт теперь задыхается в тесном втором отсеке, а SSD с системой - в прохладном первом.
В целом, операция прошла успешно, скорость действительно впечатляет

а спустя более года Windows 7 Home Premium грузится вот так:



Думаю, если ноут служит основным компом - 2 отсека обязательны. В первый - SSD с системой, а во второй - тихий неэнергоемкий террабайтник.

четверг, 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 из командной строки оказался неспособен сконвертировать созданный плейлист. Буду искать консольный конвертер.

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

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

Какое-то время назад заметил, то совершаю много рутинных действий связанных с различными желаниями. Вот, такое простое желание как "посмотреть бы хороший фильм какой-нибудь" может вылиться в достаточно долгую прелюдию по выбору фильма по продолжительности сопоставимой с длиной самого фильма. Так как смотреть всякую ерунду не хочется, приходиться идти за рекомендациями: кассовые сборы, рейтинги, награды и т.п. В этом как правило помогает 3 сайта: IMDB (для зарубежного кино), КиноПоиск (для отечественного) и Википедия (она вообще всегда помогает). После того как фильм найден, я иду в магазин и покупаю blu-ray disk. Ой, нет. Скачиваю его с торрентов предварительно пожертвовав создателям фильма электронные деньги. Так как современное кино при просмотре не на ноутбуке (а может и там тоже?) стоит смотреть только в качестве высокого разрешения, размер фильма может быть достаточно приличный - от 8 до 25-30 Гб (и это Rip, а Remux раза в полтора больше). Фильм ставится на закачку и через пару часов смотреть уже ничего не хочется. Есть шанс, что во второй раз я о нем вспомню, но момент будет уже не тот.

Итак, зачем такое долгое вступление и причем тут Python.

Захотелось это всё автоматизировать. Чтобы было так: захотелось киношку посмотреть - выбрал фильм  (из уже предварительно отобранных по какому-нибудь критерию) - включил - смотришь. Или вообще случайный фильм запустил - знаешь что они все стоящие.
Начал думать над реализацией. Нужен поток информации о новом кино, появляющемся на трекерах - например через RSS рассылку. Формальный критерий - рейтинг фильма на IMDB. Если проходит - ставить на закачку и писать и-мэйл себе на почту с описанием фильма. Запускать периодически, чтобы новые появляющиеся раздачи-фильмы анализировались.

Почему Python? А потому что Java надоела :)

Поехали. Для разбора RSS используем feedparser. Ничего проще и мощнее, по-моему не бывает.
Теперь IMDB рейтинг - в описании к раздачам его иногда указывают, но требование это необязательное. Вот название раздачи создается по вполне стандартным правилам (я сейчас о рутрекере):
Русское название / Оригинальное название (можно несколько) (Режиссер по-русски / Режиссер по-нерусски) [год, всякие разные детали, тип рипа].

Соответственно остается распарсить название фильма и спросить у IMDB его рейтинг. Здесь первый сюрприз - IMDB рейтинг так просто не отдает. Но есть недокументированная возможность найти IMDB ID фильма через запрос и JSON/XML ответ. Что и используем.
Итак, у нас есть IMDB ID фильма - загрузим страничку и распарсим рейтинг? Есть метод проще. Целых 2. Тоже по запросу возвращают JSON/XML результаты с IMDB деталями фильма. Сервисы существуют на энергии энтузиастов, поэтому решил использовать оба - на случай если один сломается. Кроме того, они с разной скоростью кэшируют IMDB-шную базу поэтому данные могут немного отличатся (в этом случае я использую те данные, где больше голосов).

Отлично, есть рейтинг фильма. Теперь нужно скачать торрент-файл и поставить его в очередь uTorrent (да, я им пользуюсь). Вторая загвоздка - авторизация на ру-трекере решилась с помощью все того же интернета - спасибо автору!
Чтобы uTorrent не задавал лишних вопросов, а просто ставил торрент на закачку, использую следующие его настройки

и параметры запуска
uTorrent.exe /MINIMIZED /DIRECTORY D:\Movies GreenMile.torrent

Теперь уведомление о новой закачке. Вначале думал о и-мейле, но потом захотелось поделиться результатами фильтрации с миром и просто поиграться с Twitter. Для твиттера нашлась прекрасная обертка для API - tweepy. Для публикации деталей (ссылка на IMDB и ссылка на раздачу) пришлось также зарегистрироваться на bitly - к которой тоже есть волшебная обертка bitlyapi.
Результат не заставил себя долго ждать.

После пары запусков обнаружилась побочная проблема - как отсеивать уже закачанные фильмы, так как полагаться на uTorrent не хотелось. Для этого я сначала прикрутил простой файловый кэш из айдишников уже скачанных фильмов. Потом понял, что могу их доставать из уже опубликованных твитов. Кэш прикрутил уже для твитов, чтобы twitter не напрягался.

Настроил анализ четырех тем с раздачами, параметры отбора фильмов можно увидеть в функции checkAndDownloadMovies() ниже.


1    __author__ = 'Smog' 
2     
3    #standard packages 
4    import json, re, urllib, urllib2, cookielib, datetime, sys, base64, subprocess, os 
5     
6    #additional packages 
7    import feedparser, requests, tweepy, bitlyapi 
8     
9    environment = { 
10       'uTorrent_location': '"C:\Program Files\uTorrent\uTorrent.exe"', 
11       'downloaded_torrents_location': 'C:\\', 
12       'downloaded_films_location': 'C:\\', 
13       'rutracker_login': '', 
14       'rutracker_password_base64': '', 
15       'downloaded_films_list_file': 'processed_items.txt', 
16    
17       'twitter_consumer_key': "", 
18       'twitter_consumer_secret': "", 
19       'twitter_access_key': "", 
20       'twitter_access_secret': "", 
21       'twitter_screen_name': '', 
22    
23       'bitly_login': '', 
24       'bitly_token': '' 
25   } 
26    
27   #------------------------------- RU TRACKER DOWNLOADER ---------------------------------------------------------------- 
28    
29   class AlreadyDownloaded(Exception): 
30       def __init__(self, value): 
31           self.value = value 
32    
33       def __str__(self): 
34           return str(self.value) 
35    
36    
37   class RuTrackerAgent: 
38       def __init__(self): 
39           self.post_params = urllib.urlencode({ 
40               'login_username': environment.get('rutracker_login'), 
41               'login_password': base64.b64decode(environment.get('rutracker_password_base64')), 
42               'login': '%C2%F5%EE%E4' 
43           }) 
44           cookie = cookielib.CookieJar() 
45           self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) 
46           urllib2.install_opener(self.opener) 
47           self.authorized = False 
48    
49       def __authorise__(self): 
50           if (not self.authorized): 
51               print 'Authorizing on RuTracker...' 
52               web_obj = self.opener.open('http://login.rutracker.org/forum/login.php', self.post_params) 
53               print 'Authorized' 
54               #        data = web_obj.read() 
55               self.authorized = True 
56    
57       def downloadTorrent(self, name, topicId, rewrite): 
58           filename = filter(lambda x: x.isalpha() or x.isdigit(), name) 
59           fname = environment.get('downloaded_torrents_location') + '\\' + filename + '.torrent' 
60    
61           if (not os.path.exists(fname)) or rewrite: 
62               f = open(fname, 'wb') 
63               self.__authorise__() 
64               print 'Downloading ' + name 
65               web_obj = self.opener.open('http://dl.rutracker.org/forum/dl.php?' + topicId, self.post_params) 
66               f.write(web_obj.read()) 
67               f.close() 
68           else: 
69               raise AlreadyDownloaded(fname) 
70    
71           return fname 
72    
73   #------------------------------- TWITTER -------------------------------------------------------------------------------- 
74    
75   class MyTweet: 
76       def __init__(self, status, string): 
77           if (status != None): 
78               self.twid = status.id 
79               m = re.search('smbid=(.)+ ', status.text) 
80               if (m != None): 
81                   self.smbid = m.group(0)[len('smbid='):-1] 
82                   print self.smbid 
83               else: 
84                   self.smbid = '' 
85    
86           if (string != None): 
87               spaceInd = string.find(' ') 
88               self.twid = string[0:spaceInd] 
89               self.smbid = string[spaceInd + 1:-1] 
90    
91    
92   class TwitterAgent: 
93       def __init__(self): 
94           auth = tweepy.OAuthHandler(environment.get('twitter_consumer_key'), environment.get('twitter_consumer_secret')) 
95           auth.set_access_token(environment.get('twitter_access_key'), environment.get('twitter_access_secret')) 
96           self.twitter = tweepy.API(auth) 
97           self.bitly = bitlyapi.BitLy(environment.get('bitly_login'), environment.get('bitly_token')) 
98           self.filename = environment.get('downloaded_films_list_file') 
99    
100      def tweet(self, message): 
101          self.twitter.update_status(message[:140]) 
102   
103      def getProcessedItemsIds(self): 
104          tweets = self.loadTweets() 
105          print 'Found ' + str(len(tweets)) + ' tweets in cache' 
106   
107          sinceId = None 
108          if (len(tweets) > 0): 
109              sinceId = tweets[0].twid 
110   
111          newTweets = self.downloadTweets(sinceId) 
112          print 'Downloaded ' + str(len(newTweets)) + ' new tweets' 
113   
114          newTweets.extend(tweets) 
115   
116          self.saveTweets(newTweets) 
117          print 'Saved ' + str(len(newTweets)) + ' tweets in cache' 
118   
119          return set(map(lambda x: x.smbid, newTweets)) 
120   
121   
122      def downloadTweets(self, sinceId): 
123          maxCount = 200 
124          lastCount = maxCount 
125          lastId = None #id of the oldest status in last call 
126          result = [] 
127   
128          while (lastCount == maxCount and lastCount > 0): 
129              if (lastId != None): 
130                  statuses = self.twitter.user_timeline(screen_name=environment.get('twitter_screen_name'), 
131                      count=maxCount, max_id=lastId - 1, since_id=sinceId) 
132              else: 
133                  statuses = self.twitter.user_timeline(screen_name=environment.get('twitter_screen_name'), 
134                      count=maxCount, since_id=sinceId) 
135   
136              result.extend(map(lambda x: MyTweet(x, None), statuses)) 
137              #            result.extend(statuses) 
138   
139              lastCount = len(statuses) 
140              if (lastCount > 0): 
141                  lastId = statuses[-1].id 
142   
143          return result 
144   
145      def loadTweets(self): 
146          try: 
147              file = open(self.filename, 'r') 
148              tweets = map(lambda x: MyTweet(None, x), file.readlines()) 
149              file.close() 
150              return tweets 
151          except: 
152              print "Could not read loaded files", sys.exc_info() 
153              return [] 
154   
155   
156      def saveTweets(self, tweets): 
157          file = open(self.filename, 'w') #environment.get('downloaded_films_list_file') 
158          for t in tweets: 
159              file.write(str(t.twid)) 
160              file.write(' ') 
161              if (t.smbid != None): 
162                  file.write(t.smbid) 
163              file.write('\n') 
164              file.flush() 
165          file.close() 
166   
167  #------------------------------- U-TORRENT ----------------------------------------------------------------------------- 
168   
169  def uTorrentRun(torrentFile, place): 
170      return subprocess.call( 
171          environment.get('uTorrent_location') + ' /MINIMIZED /DIRECTORY "' + place + '" "' + torrentFile + '" ') 
172   
173  #------------------------------- MOVIES -------------------------------------------------------------------------------- 
174   
175  class Feed: 
176      def __init__(self, title, headerFilter, ratingFilter, url): 
177          self.title = title 
178          self.ratingFilter = ratingFilter 
179          self.url = url 
180          self.headerFilter = headerFilter 
181   
182      def __str__(self): 
183          return self.title.__str__() 
184   
185   
186  class Film: 
187      def __init__(self, rssEntry, feed): 
188      #        print rssEntry 
189          m = re.search('(/ [^/]+\()', rssEntry.title) 
190          if (m != None): 
191              self.name = m.group(0)[2:-1].strip() 
192          else: 
193              self.name = 'Parsing error' 
194              print 'Could not parse name!' 
195              print rssEntry.title 
196   
197          m = re.search('^[^/]+ /', rssEntry.title) 
198          if (m != None): 
199              self.runame = m.group(0)[:-1].strip() 
200          else: 
201              self.runame = self.name 
202              print 'Could not parse Russian name!' 
203              print rssEntry.title 
204   
205          m = re.search('(\[\d\d\d\d[\, ])', rssEntry.title) 
206          if (m != None): 
207              self.year = m.group(0)[1:5].strip() 
208          else: 
209              self.year = 1812 
210              print 'Could not parse year!!' 
211              print rssEntry.title 
212   
213          m = re.search('(/ [^/\)]+\))', rssEntry.title) 
214          if (m != None): 
215              self.director = m.group(0)[2:-1].strip() 
216          else: 
217              self.director = 'Ivan Petrov-Vodkin' 
218              print 'Could not parse director!!!' 
219              print rssEntry.title 
220   
221          self.topicId = rssEntry.link[len('http://rutracker.org/forum/viewtopic.php?'):] 
222          self.topicURL = rssEntry.link 
223   
224          self.rssFeed = feed 
225   
226      def __str__(self): 
227          result = map(lambda x: x + "=" + unicode(self.__dict__.get(x)), self.__dict__.keys()) 
228          result.sort() 
229   
230          return result.__str__() 
231   
232      def loadIMDBID(self): 
233          imdbRequest = requests.get('http://www.imdb.com/xml/find?json=1&nr=1&tt=on&q=' + self.name) 
234   
235          imdbDetails = json.loads(imdbRequest.content) 
236   
237          descriptionArray = [] 
238          if 'title_exact' in imdbDetails: 
239              descriptionArray.extend(imdbDetails.get('title_exact')) 
240          if 'title_popular' in imdbDetails: 
241              descriptionArray.extend(imdbDetails.get('title_popular')) 
242   
243          if len(descriptionArray) > 1: 
244              descriptionArray = [x for x in descriptionArray if 
245                                  x.get('title_description').startswith(self.year)] 
246              if len(descriptionArray) > 1: 
247                  descriptionArray = [x for x in descriptionArray if 
248                                      x.get('title_description').lower().find(self.director.lower()) >= 0] 
249   
250          if len(descriptionArray) == 0: 
251              print 'NO MATCHES FOR ' + self.__str__() 
252              self.imdbID = 'NULL' 
253  /span>            return 
254   
255          try: 
256              self.imdbID = descriptionArray[0].get('id') 
257          except: 
258              print self 
259              print imdbDetails 
260              print descriptionArray 
261   
262      def loadRating(self): 
263          try: 
264              omdbRequest = requests.get("http://www.omdbapi.com/?i=" + self.imdbID) 
265              omdbDetails = json.loads(omdbRequest.content) 
266              try: 
267                  self.votes = float(filter(lambda x: x.isdigit(), omdbDetails.get('imdbVotes'))) 
268              except ValueError: 
269                  print "OMDB VOTES " + omdbDetails.get('imdbVotes') + " for " + self.__str__() 
270                  self.votes = 0.0 
271              try: 
272                  self.rating = float(omdbDetails.get('imdbRating')) 
273              except ValueError: 
274                  print "OMDB RATING " + omdbDetails.get('imdbRating') + " for " + self.__str__() 
275                  self.rating = 0.0 
276          except: 
277              self.rating = 0.0 
278              self.votes = 0.0 
279              print "Unexpected error:", sys.exc_info() 
280   
281          try: 
282              dcIMDBrequest = requests.get("http://www.deanclatworthy.com/imdb/?id=" + self.imdbID) 
283              dcIMDBDetails = json.loads(dcIMDBrequest.content) 
284   
285              try: 
286                  newvotes = float(filter(lambda x: x.isdigit(), dcIMDBDetails.get('votes'))) 
287              except ValueError: 
288                  print "DC VOTES " + dcIMDBDetails.get('votes') + " for " + self.__str__() 
289                  newvotes = 0.0 
290              try: 
291                  newrating = float(dcIMDBDetails.get('rating')) 
292              except ValueError: 
293                  print "DC RATING " + dcIMDBDetails.get('rating') + " for " + self.__str__() 
294                  newrating = 0.0 
295          except: 
296              newrating = 0.0 
297              newvotes = 0.0 
298              print "Unexpected error:", sys.exc_info() 
299   
300          print str(self.rating) + " vs. " + str(newrating) + "\t" + str(self.votes) + " vs. " + str(newvotes) 
301   
302          if (newvotes > self.votes and newrating > 0.0): 
303              self.rating = newrating 
304              self.votes = newvotes 
305   
306      def getTweet(self, bitly): 
307          imdbLink = bitly.shorten(longUrl='http://www.imdb.com/title/' + self.imdbID)['url'] 
308          torrentLink = bitly.shorten(longUrl=self.topicURL)['url'] 
309   
310          leftPart = self.runame + ' (' + str(self.year) + ', ' + self.director + ') ' 
311          rightPart = ' ' + imdbLink + ' ' + torrentLink + ' smbid=' + self.imdbID + ' #movie' 
312   
313          return leftPart[:140 - len(rightPart)] + rightPart 
314   
315  #------------------------------- FILMS MAIN --------------------------------------------------------------------------- 
316   
317  def checkAndDownloadMovies(): 
318      startTime = datetime.datetime.now() 
319   
320      print 'Loading RSS...' 
321      feeds = [ 
322          Feed('Latest movies', lambda title: 'BDRip'.lower() in title.lower() and '1080p'.lower() in title.lower(), 
323              lambda film: (film.rating >= 7.5 and film.votes >= 10000) or (film.rating >= 8.8), 
324              'http://feed.rutracker.org/atom/f/313.atom'), 
325          Feed('Classic movies', lambda title: 'BDRip'.lower() in title.lower() and ( 
326              '720p'.lower() in title.lower() or '1080p' in title.lower()), 
327              lambda film: (film.rating >= 7.8 and film.votes >= 15000) or (film.rating >= 8.9), 
328              'http://feed.rutracker.org/atom/f/2199.atom'), 
329          Feed('Author movies', lambda title: 'BDRip'.lower() in title.lower() and ( 
330              '720p'.lower() in title.lower() or '1080p' in title.lower()), 
331              lambda film: film.rating >= 7.7 and film.votes >= 5000, 
332              'http://feed.rutracker.org/atom/f/2339.atom'), 
333          Feed('Asian movies', lambda title: 'BDRip'.lower() in title.lower() and '1080p'.lower() in title.lower(), 
334              lambda film: film.rating >= 7.8 and film.votes >= 10000, 
335              'http://feed.rutracker.org/atom/f/2201.atom') 
336      ] 
337   
338      films = [] 
339      for rf in feeds: 
340          filmsRss = feedparser.parse(rf.url) 
341          print 'Loaded ' + str(len(filmsRss.entries)) + ' films from "' + rf.title + '"' 
342   
343          filteredEntries = filter(lambda x: rf.headerFilter(x.title), filmsRss.entries) 
344          print 'Saved ' + str(len(filteredEntries)) + ' after filtering' 
345   
346          films.extend(map(lambda x: Film(x, rf), filteredEntries)) 
347   
348      print '\nLooking for IMDB ID (' + str(len(films)) + ')...' 
349      for f in films: 
350          f.loadIMDBID() 
351          print f 
352   
353      print 'Authorizing on Twitter...' 
354      twitter = TwitterAgent() 
355      oldFilmIds = twitter.getProcessedItemsIds() 
356   
357      films = filter(lambda x: x.imdbID != 'NULL' and x.imdbID not in oldFilmIds, films) 
358   
359      print '\nLoading IMDB ratings (' + str(len(films)) + ')...' 
360      for f in films: 
361          f.loadRating() 
362          print f 
363   
364      films = filter(lambda x: x.rssFeed.ratingFilter(x), films) 
365   
366      if (len(films) > 0): 
367          print '\nLoading torrents (' + str(len(films)) + ')...' 
368   
369          agent = RuTrackerAgent() 
370   
371          for f in films: 
372              torrent = agent.downloadTorrent(f.name, f.topicId, True) 
373   
374              print 'Run uTorrent for ' + f.name 
375              uTorrentRun(torrent, environment.get('downloaded_films_location')) 
376   
377              print 'Tweet about ' + f.name 
378              twitter.tweet(f.getTweet(twitter.bitly)) 
379      else: 
380          print '\nNothing new :(' 
381   
382      print '\nFinished (total time=' + str(datetime.datetime.now() - startTime) + ")" 
383   
384  #------------------------- MAIN ---------------------------------------------------------------------------------------- 
385   
386  checkAndDownloadMovies() 
387  


Кинопоиск API не имеет, поэтому отошел на второй план. Чтобы анализировать отвечественные фильмы, нужно генерить браузеро-подобный GET и парсить страничку. Скучно. Но прикручу обязательно.

В процессе написания родилась еще одна идея - уже про музыку - о которой в следующий раз.