Какое-то время назад заметил, то совершаю много рутинных действий связанных с различными желаниями. Вот, такое простое желание как "посмотреть бы хороший фильм какой-нибудь" может вылиться в достаточно долгую прелюдию по выбору фильма по продолжительности сопоставимой с длиной самого фильма. Так как смотреть всякую ерунду не хочется, приходиться идти за рекомендациями: кассовые сборы, рейтинги, награды и т.п. В этом как правило помогает 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() ниже.
Кинопоиск API не имеет, поэтому отошел на второй план. Чтобы анализировать отвечественные фильмы, нужно генерить браузеро-подобный GET и парсить страничку. Скучно. Но прикручу обязательно.
В процессе написания родилась еще одна идея - уже про музыку - о которой в следующий раз.
Итак, зачем такое долгое вступление и причем тут 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 и парсить страничку. Скучно. Но прикручу обязательно.
В процессе написания родилась еще одна идея - уже про музыку - о которой в следующий раз.
Комментариев нет:
Отправить комментарий