Итак, зачем такое долгое вступление и причем тут
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