lol
why?
No files matched your search
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
import Content
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
def make_category_dict():
|
||||
category_dict = {
|
||||
'movies': ('Forieng Movies', '/films/fl_foreign_hight/?'),
|
||||
'rus_movies': ('Russian Movies', '/films/fl_our_hight/?'),
|
||||
'tvshows': ('TV Shows', '/serials/fl_hight/?'),
|
||||
'cartoons': ('Cartoons', '/cartoons/fl_hight/?'),
|
||||
'anime': ('Anime', '/cartoons/cartoon_genre/anime/?'),
|
||||
'hot': ('Hot & New', '/films/fl_hight/?'),
|
||||
'top': ('Top 250 Movies', '/films/fl_hight/?sort=popularity&'),
|
||||
'genre': {'genre': 'by Genre',
|
||||
'action': ('Action', '/films/film_genre/bojevik/?'),
|
||||
'adventure': ('Adventure', '/films/film_genre/priklucheniya/?'),
|
||||
'biography': ('Biography', '/films/film_genre/biografiya/?'),
|
||||
'comedy': ('Comedy', '/films/film_genre/komediya/?'),
|
||||
'crime': ('Crime', '/films/film_genre/detektiv/?'),
|
||||
'documentary': ('Documentary', '/films/film_genre/dokumentalnyj/?'),
|
||||
'drama': ('Drama', '/films/film_genre/drama/?'),
|
||||
'erotika': ('Adult', '/films/film_genre/erotika/?'),
|
||||
'family': ('Family', '/films/film_genre/semejnyj/?'),
|
||||
'fantasy': ('Fantasy', '/films/film_genre/fentezi/?'),
|
||||
'film_noir': ('Film-Noir', '/films/film_genre/nuar/?'),
|
||||
'history': ('History', '/films/film_genre/istoriya/?'),
|
||||
'horror': ('Horror', '/films/film_genre/uzhasy/?'),
|
||||
'kids': ('For Kids', '/films/film_genre/detskij/?'),
|
||||
'musical': ('Musical', '/films/film_genre/muzikl/?'),
|
||||
'mystery': ('Mystery', '/films/film_genre/mistika/?'),
|
||||
'romance': ('Romance', '/films/film_genre/melodrama/?'),
|
||||
'sci_fi': ('Sci-Fi', '/films/film_genre/fantastika/?'),
|
||||
'short': ('Short', '/films/film_genre/korotkometrazhka/?'),
|
||||
'thriller': ('Thriller', '/films/film_genre/triller/?'),
|
||||
'war': ('War', '/films/film_genre/vojennyj/?'),
|
||||
'western': ('Western', '/films/film_genre/vestern/?'),
|
||||
}
|
||||
}
|
||||
|
||||
for category in category_dict.keys():
|
||||
if isinstance(category_dict.get(category), dict):
|
||||
for subcategory in category_dict.get(category).keys():
|
||||
if subcategory != category:
|
||||
x = category_dict[category][subcategory]
|
||||
category_dict[category][subcategory] = (
|
||||
x[0], x[1] + 'view=list', {'page': x[1] + 'view=list&page=%d', 'increase': 1, 'second_page': 1})
|
||||
if not isinstance(category_dict.get(category), dict):
|
||||
x = category_dict[category]
|
||||
category_dict[category] = (
|
||||
x[0], x[1] + 'view=list', {'page': x[1] + 'view=list&page=%d', 'increase': 1, 'second_page': 1})
|
||||
|
||||
category_dict['year'] = {'year': 'by Year', }
|
||||
for y in range(2015, 1970, -1):
|
||||
category_dict['year'][str(y)] = (str(y), '/films/year/%s/' % str(y),
|
||||
{'page': '/films/year/%s/' % str(y) + '?view=list&page=%d', 'increase': 1,
|
||||
'second_page': 1})
|
||||
|
||||
return category_dict
|
||||
|
||||
|
||||
class CXZ(Content.Content):
|
||||
category_dict = make_category_dict()
|
||||
|
||||
regex_list = []
|
||||
|
||||
baseurl = "http://cxz.to"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', baseurl), ('Accept-Encoding', 'gzip'), ('Accept-Language', 'ru,en;q=0.8')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 2
|
||||
|
||||
|
||||
def isLabel(self):
|
||||
return False
|
||||
|
||||
def isPages(self):
|
||||
return True
|
||||
|
||||
def isSearchOption(self):
|
||||
return True
|
||||
|
||||
def isScrappable(self):
|
||||
return True
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
contentList = []
|
||||
url = self.get_url(category, subcategory, page, self.baseurl)
|
||||
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category:
|
||||
contentList = self.mode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def mode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('div', {'class': 'b-poster-tile '})
|
||||
num = 0
|
||||
for tr in result:
|
||||
#main
|
||||
info = {}
|
||||
year = 0
|
||||
num = num + 1
|
||||
title = tr.find('span', 'b-poster-tile__title-full').text.strip()
|
||||
originaltitle = None
|
||||
year = re.compile('(\d\d\d\d)').findall(tr.find('span', 'b-poster-tile__title-info-items').text)[0]
|
||||
link = tr.find('a', 'b-poster-tile__link').get('href')
|
||||
for i in ['/serials/', '/cartoonserials/', '/tvshow/']:
|
||||
if i in link:
|
||||
info['tvshowtitle'] = title
|
||||
break
|
||||
|
||||
img = tr.find('img').get('src')
|
||||
img = img if img else ''
|
||||
|
||||
#info
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (251 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
#print result
|
||||
return contentList
|
||||
|
||||
|
||||
'''
|
||||
- Video Values:
|
||||
- genre : string (Comedy)
|
||||
- year : integer (2009)
|
||||
- episode : integer (4)
|
||||
- season : integer (1)
|
||||
- top250 : integer (192)
|
||||
- rating : float (6.4) - range is 0..10
|
||||
- cast : list (Michal C. Hall)
|
||||
- castandrole : list (Michael C. Hall|Dexter)
|
||||
- director : string (Dagur Kari)
|
||||
- mpaa : string (PG-13)
|
||||
- plot : string (Long Description)
|
||||
- plotoutline : string (Short Description)
|
||||
- title : string (Big Fan)
|
||||
- originaltitle : string (Big Fan)
|
||||
- sorttitle : string (Big Fan)
|
||||
- duration : string (3:18)
|
||||
- studio : string (Warner Bros.)
|
||||
- tagline : string (An awesome movie) - short description of movie
|
||||
- writer : string (Robert D. Siegel)
|
||||
- tvshowtitle : string (Heroes)
|
||||
- premiered : string (2005-03-04)
|
||||
- status : string (Continuing) - status of a TVshow
|
||||
- code : string (tt0110293) - IMDb code
|
||||
- aired : string (2008-12-07)
|
||||
- credits : string (Andy Kaufman) - writing credits
|
||||
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
- album : string (The Joshua Tree)
|
||||
- artist : list (['U2'])
|
||||
- votes : string (12345 votes)
|
||||
- trailer : string (/home/user/trailer.avi)
|
||||
- dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'''
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import Content, re
|
||||
|
||||
class EZTV(Content.Content):
|
||||
category_dict = {
|
||||
'hot': ('Hot & New', '/', {'page': '/page_%d', 'increase': 1, 'second_page': 1}),
|
||||
}
|
||||
|
||||
baseurl = "https://eztv.it"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', 'https://eztv.it/'), ('Accept-Encoding', 'gzip')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 1
|
||||
|
||||
def isLabel(self):
|
||||
return True
|
||||
|
||||
def isScrappable(self):
|
||||
return False
|
||||
|
||||
def isInfoLink(self):
|
||||
return True
|
||||
|
||||
def isPages(self):
|
||||
return True
|
||||
|
||||
def isSearchOption(self):
|
||||
return False
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
contentList = []
|
||||
url = self.get_url(category, subcategory, page, self.baseurl)
|
||||
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category in ['hot']:
|
||||
contentList = self.mode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def mode(self, response):
|
||||
contentList = []
|
||||
#print str(result)
|
||||
num = 51
|
||||
result = re.compile(
|
||||
r'''class="epinfo">(.+?)</a>.+?<a href="(magnet.+?)"''',
|
||||
re.DOTALL).findall(response)
|
||||
for title, link in result:
|
||||
#main
|
||||
info = {}
|
||||
num = num - 1
|
||||
original_title = None
|
||||
year = 0
|
||||
img = ''
|
||||
#info
|
||||
|
||||
info['label'] = info['title'] = title
|
||||
info['link'] = link
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (int(num))),
|
||||
original_title, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
import Content
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
class FastTorrent(Content.Content):
|
||||
category_dict = {
|
||||
#'movies':('Movies', '/popular/'),
|
||||
'tvshows': (
|
||||
'TV Shows', '/last-tv-torrent/', {'page': '/last-tv-torrent/%d.html', 'increase': 1, 'second_page': 2}),
|
||||
'cartoons': ('Cartoons', '/last-multfilm-torrent/',
|
||||
{'page': '/last-multfilm-torrent/%d.html', 'increase': 1, 'second_page': 2}),
|
||||
'anime': ('Anime', '/anime/multfilm/', {'page': '/anime/multfilm/%d.html', 'increase': 1, 'second_page': 2}),
|
||||
'hot': ('Hot & New', '/new-films/', {'page': '/new-films/%d.html', 'increase': 1, 'second_page': 2}),
|
||||
'genre': {'genre': 'by Genre',
|
||||
'amime_series': ('Anime Series', '/anime-serialy/multfilm/',
|
||||
{'page': '/anime-serialy/multfilm/%d.html', 'increase': 1, 'second_page': 2}),
|
||||
}
|
||||
#'top':('Top 250 Movies', '/top/'),
|
||||
}
|
||||
baseurl = "http://www.fast-torrent.ru"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', baseurl), ('Accept-Encoding', 'gzip'), ('Accept-Language', 'ru,en;q=0.8')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 1
|
||||
|
||||
def isLabel(self):
|
||||
return False
|
||||
|
||||
def isScrappable(self):
|
||||
return True
|
||||
|
||||
def isSearchOption(self):
|
||||
return True
|
||||
|
||||
def isInfoLink(self):
|
||||
return False
|
||||
|
||||
def isPages(self):
|
||||
return True
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
contentList = []
|
||||
if not subcategory or subcategory == True:
|
||||
get = self.category_dict[category]
|
||||
else:
|
||||
get = self.category_dict[category][subcategory]
|
||||
|
||||
if not page or page == 1:
|
||||
url = self.baseurl + get[1]
|
||||
else:
|
||||
property = self.get_property(category, subcategory)
|
||||
|
||||
page_url = property['page'] % (property['second_page'] + ((page - 2) * property['increase']))
|
||||
url = self.baseurl + str(page_url)
|
||||
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category: # in ['hot']:
|
||||
contentList = self.popmode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def popmode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response.decode('utf-8'))
|
||||
result = Soup.findAll('div', {'class': 'film-wrap'})
|
||||
num = 16
|
||||
for tr in result:
|
||||
#main
|
||||
info = {}
|
||||
num = num - 1
|
||||
original_title = None
|
||||
year = 0
|
||||
h2 = tr.find('h2')
|
||||
#print str(h2)
|
||||
label = h2.find('span', {'itemprop': 'name'})
|
||||
if label:
|
||||
title = label.text
|
||||
year = re.compile('\((\d\d\d\d)\)').findall(h2.text)
|
||||
if year:
|
||||
year = year[0]
|
||||
original_title = h2.find('span', {'itemprop': 'alternativeHeadline'})
|
||||
if original_title:
|
||||
original_title = original_title.text
|
||||
else:
|
||||
try:
|
||||
title, year, original_title = \
|
||||
re.compile(u'<h2>(.+?) \((\d\d\d\d)\) <br.*?>\((.+?)\)<', re.DOTALL | re.I).findall(unicode(h2))[0]
|
||||
except:
|
||||
try:
|
||||
title, year = re.compile(u'<h2>(.+?) \((\d\d\d\d)\)<', re.DOTALL | re.I).findall(unicode(h2))[0]
|
||||
except:
|
||||
pass
|
||||
a = tr.find('div', 'film-image').find('a')
|
||||
link = a.get('href')
|
||||
img = a.get('style')
|
||||
if img:
|
||||
img = img.replace('background: url(', '').rstrip(')')
|
||||
|
||||
#info
|
||||
|
||||
info['label'] = title
|
||||
info['link'] = link
|
||||
info['title'] = title
|
||||
genre = tr.find('div', 'film-genre').text
|
||||
tv = [u'Зарубежный сериал', u'Русский сериал', u'Аниме сериалы', u'Мультсериалы']
|
||||
for i in tv:
|
||||
if re.search(i, genre):
|
||||
info['tvshowtitle'] = title
|
||||
info['year'] = int(year)
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (int(num))),
|
||||
original_title, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
@@ -0,0 +1,248 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import HTMLParser
|
||||
|
||||
import Content
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
class IMDB(Content.Content):
|
||||
category_dict = {
|
||||
'movies': ('Forieng Movies', '/search/title?languages=en|1&title_type=feature&sort=moviemeter,asc'),
|
||||
'rus_movies': ('Russian Movies', '/search/title?languages=ru|1&title_type=feature&sort=moviemeter,asc'),
|
||||
'tvshows': ('TV Shows', '/search/title?count=100&title_type=tv_series,mini_series&ref_=gnr_tv_mp'),
|
||||
'cartoons': ('Cartoons', '/search/title?genres=animation&title_type=feature&sort=moviemeter,asc'),
|
||||
'anime': ('Anime',
|
||||
'/search/title?count=100&genres=animation&keywords=anime&num_votes=1000,&explore=title_type&ref_=gnr_kw_an'),
|
||||
'hot': ('Hot & New', '/search/title?count=100&title_type=feature%2Ctv_series%2Ctv_movie&ref_=nv_ch_mm_1'),
|
||||
'top': ('Top 250 Movies', '/chart/top/'),
|
||||
'search': ('[B]Search[/B]', '/find?q=%s&s=tt&ttype=ft'),
|
||||
'year': {'year': 'by Year', },
|
||||
'genre': {'genre': 'by Genre',
|
||||
'action': ('Action', '/genre/action'),
|
||||
'adventure': ('Adventure', '/genre/adventure'),
|
||||
'animation': ('Animation', '/genre/animation'),
|
||||
'biography': ('Biography', '/genre/biography'),
|
||||
'comedy': ('Comedy', '/genre/comedy'),
|
||||
'crime': ('Crime', '/genre/crime'),
|
||||
'documentary': ('Documentary', '/genre/documentary'),
|
||||
'drama': ('Drama', '/genre/drama'),
|
||||
'family': ('Family', '/genre/family'),
|
||||
'fantasy': ('Fantasy', '/genre/fantasy'),
|
||||
'film_noir': ('Film-Noir', '/genre/film_noir'),
|
||||
'history': ('History', '/genre/history'),
|
||||
'horror': ('Horror', '/genre/horror'),
|
||||
'music': ('Music', '/genre/music'),
|
||||
'musical': ('Musical', '/genre/musical'),
|
||||
'mystery': ('Mystery', '/genre/mystery'),
|
||||
'romance': ('Romance', '/genre/romance'),
|
||||
'sci_fi': ('Sci-Fi', '/genre/sci_fi'),
|
||||
'short': ('Short', '/genre/short'),
|
||||
'sport': ('Sport', '/genre/sport'),
|
||||
'thriller': ('Thriller', '/genre/thriller'),
|
||||
'war': ('War', '/genre/war'),
|
||||
'western': ('Western', '/genre/western'),
|
||||
}
|
||||
}
|
||||
|
||||
for y in range(2015, 1970, -1):
|
||||
category_dict['year'][str(y)] = (str(y), '/year/%s/' % str(y))
|
||||
|
||||
regex_list = []
|
||||
|
||||
baseurl = "http://imdb.com"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', baseurl), ('Accept-Encoding', 'gzip'), ('Accept-Language', 'ru,en;q=0.8')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 2
|
||||
|
||||
def isLabel(self):
|
||||
return False
|
||||
|
||||
def isPages(self):
|
||||
return False
|
||||
|
||||
def isSearchOption(self):
|
||||
return True
|
||||
|
||||
def isScrappable(self):
|
||||
return True
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
contentList = []
|
||||
url = self.get_url(category, subcategory, page, self.baseurl)
|
||||
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category in ['top']:
|
||||
contentList = self.topmode(response)
|
||||
elif category == 'search':
|
||||
contentList = self.searchmode(response)
|
||||
else: #if category in ['genre']:
|
||||
contentList = self.genremode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def searchmode(self, response):
|
||||
contentList = []
|
||||
pars = HTMLParser.HTMLParser()
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('tr', {'class': ['findResult odd', 'findResult even']})
|
||||
num = 250
|
||||
for tr in result:
|
||||
#main
|
||||
info = {}
|
||||
year = 0
|
||||
num = num - 1
|
||||
|
||||
title = pars.unescape(tr.findAll('a')[1].text)
|
||||
tdtitle = tr.find('td', 'result_text')
|
||||
#print str(tdtitle.text.encode('utf-8'))
|
||||
originaltitle = tr.find('i')
|
||||
if originaltitle:
|
||||
originaltitle = originaltitle.text
|
||||
try:
|
||||
year = re.compile('\((\d\d\d\d)\)').findall(tdtitle.text)[0]
|
||||
except:
|
||||
try:
|
||||
year = re.compile('(\d\d\d\d)').findall(tdtitle.text)[0]
|
||||
except:
|
||||
pass
|
||||
info['tvshowtitle'] = title
|
||||
img = self.biggerImg(tr.find('img').get('src'))
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (251 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
#print result
|
||||
return contentList
|
||||
|
||||
def genremode(self, response):
|
||||
contentList = []
|
||||
pars = HTMLParser.HTMLParser()
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('tr', {'class': ['odd detailed', 'even detailed']})
|
||||
for tr in result:
|
||||
#main
|
||||
info = {}
|
||||
year = 0
|
||||
tdtitle = tr.find('td', 'title')
|
||||
num = tr.find('td', 'number').text.rstrip('.')
|
||||
originaltitle = None
|
||||
title = pars.unescape(tdtitle.find('a').text)
|
||||
try:
|
||||
year = re.compile('\((\d\d\d\d)\)').findall(tdtitle.find('span', 'year_type').text)[0]
|
||||
except:
|
||||
try:
|
||||
year = re.compile('(\d\d\d\d)').findall(tdtitle.find('span', 'year_type').text)[0]
|
||||
except:
|
||||
pass
|
||||
info['tvshowtitle'] = title
|
||||
img = self.biggerImg(tr.find('td', 'image').find('img').get('src'))
|
||||
|
||||
#info
|
||||
|
||||
info['code'] = tr.find('span', 'wlb_wrapper').get('data-tconst')
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (251 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
#print result
|
||||
return contentList
|
||||
|
||||
def biggerImg(self, img):
|
||||
if img and '._' in img:
|
||||
img = img.split('._')[0] + '._V1_SY_CR1,0,,_AL_.jpg'
|
||||
return img
|
||||
|
||||
def topmode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('tr', {'class': ['odd', 'even']})
|
||||
for tr in result:
|
||||
#main
|
||||
tdtitle = tr.find('td', 'titleColumn')
|
||||
num = tdtitle.find('span', {'name': 'ir'}).text.rstrip('.')
|
||||
originaltitle = None
|
||||
title = tdtitle.find('a').text
|
||||
year = tdtitle.find('span', {'name': 'rd'}).text.rstrip(')').lstrip('(')
|
||||
tdposter = tr.find('td', 'posterColumn')
|
||||
img = self.biggerImg(tdposter.find('img').get('src'))
|
||||
|
||||
#info
|
||||
info = {}
|
||||
info['title'] = title
|
||||
info['year'] = int(year)
|
||||
info['code'] = tr.find('div', 'wlb_ribbon').get('data-tconst')
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (251 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
#print result
|
||||
return contentList
|
||||
|
||||
|
||||
'''
|
||||
- Video Values:
|
||||
- genre : string (Comedy)
|
||||
- year : integer (2009)
|
||||
- episode : integer (4)
|
||||
- season : integer (1)
|
||||
- top250 : integer (192)
|
||||
- rating : float (6.4) - range is 0..10
|
||||
- cast : list (Michal C. Hall)
|
||||
- castandrole : list (Michael C. Hall|Dexter)
|
||||
- director : string (Dagur Kari)
|
||||
- mpaa : string (PG-13)
|
||||
- plot : string (Long Description)
|
||||
- plotoutline : string (Short Description)
|
||||
- title : string (Big Fan)
|
||||
- originaltitle : string (Big Fan)
|
||||
- sorttitle : string (Big Fan)
|
||||
- duration : string (3:18)
|
||||
- studio : string (Warner Bros.)
|
||||
- tagline : string (An awesome movie) - short description of movie
|
||||
- writer : string (Robert D. Siegel)
|
||||
- tvshowtitle : string (Heroes)
|
||||
- premiered : string (2005-03-04)
|
||||
- status : string (Continuing) - status of a TVshow
|
||||
- code : string (tt0110293) - IMDb code
|
||||
- aired : string (2008-12-07)
|
||||
- credits : string (Andy Kaufman) - writing credits
|
||||
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
- album : string (The Joshua Tree)
|
||||
- artist : list (['U2'])
|
||||
- votes : string (12345 votes)
|
||||
- trailer : string (/home/user/trailer.avi)
|
||||
- dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'''
|
||||
@@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import socket
|
||||
|
||||
import Content
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
class KinoPoisk(Content.Content):
|
||||
category_dict = {
|
||||
'tvshows': ('TV Shows', '/top/serial/list/'),
|
||||
'cartoons': ('Cartoons', '/top/id_genre/14/'),
|
||||
'search': ('[B]Search[/B]', '/s/type/film/list/1/find/%s/'),
|
||||
'movies': ('Forieng Movies', '/s/type/film/list/1/m_act[country]/1/m_act[type]/film/'),
|
||||
'rus_movies': ('Russian Movies', '/s/type/film/list/1/m_act[country]/2/m_act[type]/film/'),
|
||||
'anime': ('Anime', '/s/type/film/list/1/order/rating/m_act[genre][0]/1750/',),
|
||||
'hot': ('Hot & New', '/popular/'),
|
||||
'top': ('Top 250 Movies', '/top/'),
|
||||
'genre': {'genre': 'by Genre',
|
||||
'russia': ('Russia & USSR', '/top/rus/list/'),
|
||||
'biography': ('Biography', '/s/type/film/list/1/m_act[genre][0]/22/'),
|
||||
'action': ('Action', '/top/id_genre/3/'),
|
||||
'thriller': ('Thriller', '/top/id_genre/4/'),
|
||||
'comedy': ('Comedy', '/top/id_genre/6/'),
|
||||
'drama': ('Drama', '/top/id_genre/8/'),
|
||||
'romance': ('Romance', '/top/id_genre/7/'),
|
||||
'horror': ('Horror', '/top/id_genre/1/'),
|
||||
'sci_fi': ('Sci-Fi', '/top/id_genre/2/'),
|
||||
'documentary': ('Documentary', '/top/id_genre/12/'),
|
||||
'cartoonseries': ('Cartoons Series', '/top/mult_serial/list/'),
|
||||
'cartoonshort': ('Cartoons Short', '/top/short_mult/list/'),
|
||||
'short': ('Short', '/top/short/list/'),
|
||||
'male': ('Male', '/top/sex/male/'),
|
||||
'female': ('Female', '/top/sex/female/'),
|
||||
}
|
||||
}
|
||||
|
||||
for category in category_dict.keys():
|
||||
if isinstance(category_dict.get(category), dict):
|
||||
for subcategory in category_dict.get(category).keys():
|
||||
if subcategory != category:
|
||||
x = category_dict[category][subcategory]
|
||||
if x[1].startswith('/s/type/film/list/'):
|
||||
category_dict[category][subcategory] = (x[0], x[1] + 'perpage/25/',
|
||||
{'page': x[1] + 'perpage/25/page/%d/', 'increase': 1,
|
||||
'second_page': 2})
|
||||
if not isinstance(category_dict.get(category), dict):
|
||||
x = category_dict[category]
|
||||
if x[1].startswith('/s/type/film/list/'):
|
||||
category_dict[category] = (
|
||||
x[0], x[1] + 'perpage/25/', {'page': x[1] + 'perpage/25/page/%d/', 'increase': 1, 'second_page': 2})
|
||||
|
||||
category_dict['year'] = {'year': 'by Year', }
|
||||
for y in range(2015, 1970, -1):
|
||||
category_dict['year'][str(y)] = (str(y), '/s/type/film/list/1/m_act[year]/%s/' % str(y) + 'perpage/25/',
|
||||
{'page': '/s/type/film/list/1/m_act[year]/%s/' % str(y) + 'perpage/25/page/%d/',
|
||||
'increase': 1, 'second_page': 2})
|
||||
|
||||
baseurl = "http://www.kinopoisk.ru"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', baseurl), ('Accept-Encoding', 'gzip'), ('Accept-Language', 'ru,en;q=0.8')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 2
|
||||
|
||||
def isLabel(self):
|
||||
return False
|
||||
|
||||
def isPages(self):
|
||||
return True
|
||||
|
||||
def isSearchOption(self):
|
||||
return True
|
||||
|
||||
def isScrappable(self):
|
||||
return True
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
socket.setdefaulttimeout(15)
|
||||
contentList = []
|
||||
url = self.get_url(category, subcategory, page, self.baseurl)
|
||||
|
||||
#print url
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category in ['hot']:
|
||||
contentList = self.popmode(response)
|
||||
elif url.startswith(self.baseurl + '/s/type/film/list/'):
|
||||
contentList = self.infomode(response)
|
||||
else:
|
||||
contentList = self.topmode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def stripTtl(self, title):
|
||||
bad_end = [u'\(ТВ\)', u'\(сериал\)', u'\(видео\)']
|
||||
for code in bad_end:
|
||||
title = re.sub(u' ' + code + '$', '', title)
|
||||
return title
|
||||
|
||||
def popmode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.find('div', 'stat').findAll('div', 'el')
|
||||
#print str(result)
|
||||
for tr in result:
|
||||
#main
|
||||
a = tr.findAll('a')
|
||||
num = a[0].text
|
||||
#print num
|
||||
info = {}
|
||||
year = 0
|
||||
img = ''
|
||||
originaltitle = tr.find('i')
|
||||
if originaltitle:
|
||||
originaltitle = self.stripHtml(self.unescape(originaltitle.text))
|
||||
title = a[1].text
|
||||
link = a[1].get('href')
|
||||
if link:
|
||||
id = re.compile('/film/(\d+)/').findall(link)
|
||||
if id:
|
||||
img = self.id2img(id[0])
|
||||
try:
|
||||
title, year = re.compile('(.+?) \((\d\d\d\d)\)', re.DOTALL).findall(a[1].text)[0]
|
||||
except:
|
||||
pass
|
||||
if not year:
|
||||
try:
|
||||
title, year = re.compile('(.+?) \(.*(\d\d\d\d)').findall(a[1].text)[0]
|
||||
info['tvshowtitle'] = title
|
||||
except:
|
||||
pass
|
||||
title = self.stripHtml(self.stripTtl(title))
|
||||
|
||||
#info
|
||||
info['title'] = title
|
||||
info['year'] = int(year)
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (201 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
|
||||
def topmode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.find('table', {'cellpadding': '3'}).findAll('tr')[2:]
|
||||
#print str(result)
|
||||
for tr in result:
|
||||
#main
|
||||
td = tr.findAll('td')
|
||||
year = 0
|
||||
info = {}
|
||||
img = ''
|
||||
title, originaltitle = None, None
|
||||
num = td[0].text.rstrip('.')
|
||||
originaltitle = tr.find('span', 'text-grey')
|
||||
if originaltitle:
|
||||
originaltitle = self.stripHtml(self.unescape(originaltitle.text))
|
||||
a_all = tr.find('a', {'class': 'all'})
|
||||
if a_all:
|
||||
link = a_all.get('href')
|
||||
if link:
|
||||
id = re.compile('/film/(\d+)/').findall(link)
|
||||
if id:
|
||||
img = self.id2img(id[0])
|
||||
year = re.compile('(.+) \((\d\d\d\d)\)').findall(a_all.text)
|
||||
if not year:
|
||||
try:
|
||||
title, year = re.compile('(.+) \(.*(\d\d\d\d)').findall(a_all.text)[0]
|
||||
info['tvshowtitle'] = title
|
||||
except:
|
||||
title = a_all.text
|
||||
else:
|
||||
title, year = year[0]
|
||||
title = self.stripHtml(self.stripTtl(title))
|
||||
|
||||
#info
|
||||
if originaltitle and not title:
|
||||
title = originaltitle
|
||||
originaltitle = None
|
||||
|
||||
if title:
|
||||
info['title'] = title
|
||||
info['year'] = int(year)
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (251 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
|
||||
def infomode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('div', 'info')
|
||||
#print str(result)
|
||||
num = 0
|
||||
for div in result:
|
||||
#main
|
||||
info = {}
|
||||
img = ''
|
||||
name = div.find('p', 'name')
|
||||
title = name.find('a').text
|
||||
link = name.find('a').get('href')
|
||||
if link:
|
||||
id = re.compile('/film/(\d+)/').findall(link)
|
||||
if id:
|
||||
img = self.id2img(id[0])
|
||||
year = name.find('span', 'year') if name.find('span', 'year') else 0
|
||||
if year:
|
||||
year=year.text
|
||||
ysplit = year.split(' ')
|
||||
if len(ysplit) > 1: year = ysplit[0]
|
||||
title = self.stripHtml(self.unescape(title))
|
||||
tvshowtitle = re.compile(u'(.+?) \((.+?)\)$').findall(title)
|
||||
if tvshowtitle and tvshowtitle[0][1] in [u'сериал']:
|
||||
title = tvshowtitle[0][0]
|
||||
info['tvshowtitle'] = title
|
||||
num = num + 1
|
||||
originaltitle = div.find('span', 'gray')
|
||||
if originaltitle:
|
||||
originaltitle = re.match('(.+?), \d', originaltitle.text)
|
||||
if originaltitle:
|
||||
originaltitle = self.stripHtml(self.unescape(originaltitle.group(1)))
|
||||
title = self.stripTtl(title)
|
||||
|
||||
#info
|
||||
info['title'] = title
|
||||
info['year'] = int(year)
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (100 - int(num))),
|
||||
originaltitle, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
|
||||
|
||||
def id2img(self, id):
|
||||
if id:
|
||||
return "http://st.kp.yandex.net/images/film_iphone/iphone360_%s.jpg" % (str(id))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
'''
|
||||
- Video Values:
|
||||
- genre : string (Comedy)
|
||||
- year : integer (2009)
|
||||
- episode : integer (4)
|
||||
- season : integer (1)
|
||||
- top250 : integer (192)
|
||||
- tracknumber : integer (3)
|
||||
- rating : float (6.4) - range is 0..10
|
||||
- watched : depreciated - use playcount instead
|
||||
- playcount : integer (2) - number of times this item has been played
|
||||
- overlay : integer (2) - range is 0..8. See GUIListItem.h for values
|
||||
- cast : list (Michal C. Hall)
|
||||
- castandrole : list (Michael C. Hall|Dexter)
|
||||
- director : string (Dagur Kari)
|
||||
- mpaa : string (PG-13)
|
||||
- plot : string (Long Description)
|
||||
- plotoutline : string (Short Description)
|
||||
- title : string (Big Fan)
|
||||
- originaltitle : string (Big Fan)
|
||||
- sorttitle : string (Big Fan)
|
||||
- duration : string (3:18)
|
||||
- studio : string (Warner Bros.)
|
||||
- tagline : string (An awesome movie) - short description of movie
|
||||
- writer : string (Robert D. Siegel)
|
||||
- tvshowtitle : string (Heroes)
|
||||
- premiered : string (2005-03-04)
|
||||
- status : string (Continuing) - status of a TVshow
|
||||
- code : string (tt0110293) - IMDb code
|
||||
- aired : string (2008-12-07)
|
||||
- credits : string (Andy Kaufman) - writing credits
|
||||
- lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
- album : string (The Joshua Tree)
|
||||
- artist : list (['U2'])
|
||||
- votes : string (12345 votes)
|
||||
- trailer : string (/home/user/trailer.avi)
|
||||
- dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04)
|
||||
'''
|
||||
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Torrenter plugin for XBMC
|
||||
Copyright (C) 2012 Vadim Skorba
|
||||
vadim.skorba@gmail.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import Content
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
class RiperAM(Content.Content):
|
||||
category_dict = {
|
||||
#'movies':('Movies', '/popular/'),
|
||||
#'tvshows':('TV Shows', '/top/serial/list/'),
|
||||
#'cartoons':('Cartoons', '/top/id_genre/14/'),
|
||||
#'anime':('Anime', '/search/title?count=100&genres=animation&keywords=anime&num_votes=1000,&explore=title_type&ref_=gnr_kw_an'),
|
||||
'hot': ('Hot & New', '/', {'page': '/portal.php?tp=%d', 'increase': 30, 'second_page': 30}),
|
||||
#'top':('Top 250 Movies', '/top/'),
|
||||
}
|
||||
|
||||
baseurl = "http://www.riper.am"
|
||||
headers = [('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124' + \
|
||||
' YaBrowser/14.10.2062.12061 Safari/537.36'),
|
||||
('Referer', 'http://www.riper.am/'), ('Accept-Encoding', 'gzip,deflate,sdch'),
|
||||
('Accept-Language', 'ru,en;q=0.8')]
|
||||
'''
|
||||
Weight of source with this searcher provided.
|
||||
Will be multiplied on default weight.
|
||||
Default weight is seeds number
|
||||
'''
|
||||
sourceWeight = 1
|
||||
|
||||
def isLabel(self):
|
||||
return True
|
||||
|
||||
def isScrappable(self):
|
||||
return False
|
||||
|
||||
def isInfoLink(self):
|
||||
return True
|
||||
|
||||
def isPages(self):
|
||||
return True
|
||||
|
||||
def isSearchOption(self):
|
||||
return False
|
||||
|
||||
def get_contentList(self, category, subcategory=None, page=None):
|
||||
contentList = []
|
||||
url = self.get_url(category, subcategory, page, self.baseurl)
|
||||
|
||||
response = self.makeRequest(url, headers=self.headers)
|
||||
|
||||
if None != response and 0 < len(response):
|
||||
#print response
|
||||
if category in ['hot']:
|
||||
contentList = self.popmode(response)
|
||||
#print str(contentList)
|
||||
return contentList
|
||||
|
||||
def popmode(self, response):
|
||||
contentList = []
|
||||
Soup = BeautifulSoup(response)
|
||||
result = Soup.findAll('table', 'postbody postbody_portal')
|
||||
#print str(result)
|
||||
num = 31
|
||||
bad_forum = [u'Безопасность', u'Книги и журналы', u'Action & Shooter', u'RPG/MMORPG']
|
||||
for tr in result:
|
||||
#main
|
||||
info = {}
|
||||
forum = tr.find('div', {'style': 'height:20px;overflow:hidden;'}).find('a').text
|
||||
if forum and forum in bad_forum:
|
||||
continue
|
||||
link = tr.find('div', {'style': 'width:200px;overflow:hidden;'}).find('a').get('href')
|
||||
num = num - 1
|
||||
label = tr.find('strong').text
|
||||
original_title = None
|
||||
year = 0
|
||||
title = self.unescape(label)
|
||||
img = tr.findAll('a')[0].find('img').get('src')
|
||||
if img:
|
||||
img = img.replace('.webp', '.jpg')
|
||||
|
||||
#info
|
||||
|
||||
info['label'] = label
|
||||
info['link'] = link
|
||||
info['title'] = title
|
||||
info['year'] = int(year)
|
||||
|
||||
contentList.append((
|
||||
int(int(self.sourceWeight) * (int(num))),
|
||||
original_title, title, int(year), img, info,
|
||||
))
|
||||
return contentList
|
||||
|
After Width: | Height: | Size: 95 B |
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<strings>
|
||||
<string id="30001">Interface Language</string>
|
||||
<string id="30002">Lock Folders View Style</string>
|
||||
<string id="30003">Off</string>
|
||||
<string id="30004">Save Files To Folder</string>
|
||||
<string id="30007">Use magnet-links</string>
|
||||
<string id="30008">Keep downloaded files</string>
|
||||
<string id="30009">Keep seeding of downloaded files</string>
|
||||
<string id="30010">Upload speed limit MBits/sec (0 - unlimited)</string>
|
||||
<string id="30011">Download speed limit MBits/sec (0 - unlimited)</string>
|
||||
<string id="30012">Use only system libtorrent</string>
|
||||
<string id="30013">Predownload and play next episode</string>
|
||||
<string id="30014">Download metadata for Content Lists</string>
|
||||
<string id="30015">Debug (Developer Mode)</string>
|
||||
<string id="30016">Confluence (by slng)</string>
|
||||
<string id="30017">Transperency (by slng)</string>
|
||||
<string id="30018">Confluence (by DiMartino)</string>
|
||||
<string id="30019">Confluence (by RussakHH)</string>
|
||||
<string id="30020">Enable Search History</string>
|
||||
<string id="30021">Python-Libtorrent (recommended)</string>
|
||||
<string id="30022">Ace Stream (no magnets)</string>
|
||||
<string id="30023">P2P Player</string>
|
||||
<string id="30024">Rest Settings in Programms - AceStream Client</string>
|
||||
<string id="30025">Search Timeout</string>
|
||||
<string id="30026">Short (10s)</string>
|
||||
<string id="30027">Normal (20s)</string>
|
||||
<string id="30028">Long (30s)</string>
|
||||
<string id="30029">Predownload subtitles from all folders</string>
|
||||
<string id="30030">Keep seeding until Kodi restart</string>
|
||||
<string id="30101">Interface</string>
|
||||
<string id="30102">P2P Network</string>
|
||||
<string id="50301">Save path</string>
|
||||
<string id="50302">Call dialog</string>
|
||||
<string id="50303">Default</string>
|
||||
<string id="50304">Path</string>
|
||||
<string id="50305">Create subdirectory for scrapper</string>
|
||||
<string id="50312">Host</string>
|
||||
<string id="50313">Port</string>
|
||||
<string id="50314">URL</string>
|
||||
<string id="50315">Login</string>
|
||||
<string id="50316">Password</string>
|
||||
<string id="50311">Torrent Client</string>
|
||||
<string id="30426">Path replacement (remote only)</string>
|
||||
<string id="30412">Close</string>
|
||||
<string id="30413">Open Settings</string>
|
||||
<string id="30414">Torrent Client Browser</string>
|
||||
|
||||
</strings>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<strings>
|
||||
<string id="30001">Язык интерфейса</string>
|
||||
<string id="30002">Удерживать стиль отображения</string>
|
||||
<string id="30003">Отключено</string>
|
||||
<string id="30004">Сохранять файлы в директорию</string>
|
||||
<string id="30007">Использовать магнет-ссылки</string>
|
||||
<string id="30008">Хранить загруженные файлы</string>
|
||||
<string id="30009">Оставаться на раздаче скачанных файлов</string>
|
||||
<string id="30010">Ограничить скорость раздачи МБит/сек (0 - неограничено)</string>
|
||||
<string id="30011">Ограничить скорость закачки МБит/сек (0 - неограничено)</string>
|
||||
<string id="30012">Использовать только пользовательский libtorrent</string>
|
||||
<string id="30013">Скачать и запустить следующий эпизод</string>
|
||||
<string id="30014">Загружать мета-данные для Списков Медиа</string>
|
||||
<string id="30015">Дебаг (Разработчик)</string>
|
||||
<string id="30016">Confluence (от slng)</string>
|
||||
<string id="30017">Transperency (от slng)</string>
|
||||
<string id="30018">Confluence (от DiMartino)</string>
|
||||
<string id="30019">Confluence (от RussakHH)</string>
|
||||
<string id="30020">Включить Историю Поиска</string>
|
||||
<string id="30021">Python-Libtorrent (предпочтительно)</string>
|
||||
<string id="30022">Ace Stream (без магнит-ссылок)</string>
|
||||
<string id="30023">P2P Проигрыватель</string>
|
||||
<string id="30024">Остальные настройки в "Программы - AceStream Client"</string>
|
||||
<string id="30025">Ожидание ответа сервера при Поиске</string>
|
||||
<string id="30026">Короткое (10с)</string>
|
||||
<string id="30027">Среднее (20с)</string>
|
||||
<string id="30028">Долгое (30с)</string>
|
||||
<string id="30029">Предзакачать и подключить субтитры</string>
|
||||
<string id="30030">Сидировать до полного выключения Kodi</string>
|
||||
<string id="30101">Интерфейс</string>
|
||||
<string id="30102">P2P Сеть</string>
|
||||
<string id="50301">Директория для сохранения файлов</string>
|
||||
<string id="50302">Вызывать диалог</string>
|
||||
<string id="50303">Задать по умолчанию</string>
|
||||
<string id="50304">Путь к директории</string>
|
||||
<string id="50305">Создавать поддиректорию для скрапера</string>
|
||||
<string id="50312">Хост</string>
|
||||
<string id="50313">Порт</string>
|
||||
<string id="50314">URL</string>
|
||||
<string id="50315">Логин</string>
|
||||
<string id="50316">Пароль</string>
|
||||
<string id="50311">Торрент-клиент</string>
|
||||
<string id="30426">Замена пути (remote only)</string>
|
||||
<string id="30412">Закрыть</string>
|
||||
<string id="30413">Открыть Настройки</string>
|
||||
<string id="30414">Браузер Торрент-клиента</string>
|
||||
|
||||
<string id="30146">Статус Плагина</string>
|
||||
</strings>
|
||||
@@ -0,0 +1,774 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This is a "local" version of PyXBMCt to be used in standalone addons.
|
||||
#
|
||||
# PyXBMCt is a mini-framework for creating XBMC Python addons with arbitrary UI
|
||||
# made of controls - decendants of xbmcgui.Control class.
|
||||
# The framework uses image textures from XBMC Confluence skin.
|
||||
#
|
||||
# Licence: GPL v.3 http://www.gnu.org/licenses/gpl.html
|
||||
#
|
||||
## @package addonwindow
|
||||
# PyXBMCt framework module
|
||||
|
||||
import os
|
||||
import xbmc, xbmcgui, xbmcaddon
|
||||
|
||||
#_addon = xbmcaddon.Addon()
|
||||
_images = os.path.join(os.path.dirname(__file__), 'textures', 'default')
|
||||
|
||||
|
||||
# Text alighnment constants. Mixed variants are obtained by bit OR (|)
|
||||
ALIGN_LEFT = 0
|
||||
ALIGN_RIGHT = 1
|
||||
ALIGN_CENTER_X = 2
|
||||
ALIGN_CENTER_Y = 4
|
||||
ALIGN_CENTER = 6
|
||||
ALIGN_TRUNCATED = 8
|
||||
ALIGN_JUSTIFY = 10
|
||||
|
||||
# XBMC key action codes.
|
||||
# More codes at https://github.com/xbmc/xbmc/blob/master/xbmc/guilib/Key.h
|
||||
## ESC action
|
||||
ACTION_PREVIOUS_MENU = 10
|
||||
## Backspace action
|
||||
ACTION_NAV_BACK = 92
|
||||
## Left arrow key
|
||||
ACTION_MOVE_LEFT = 1
|
||||
## Right arrow key
|
||||
ACTION_MOVE_RIGHT = 2
|
||||
## Up arrow key
|
||||
ACTION_MOVE_UP = 3
|
||||
## Down arrow key
|
||||
ACTION_MOVE_DOWN = 4
|
||||
## Mouse wheel up
|
||||
ACTION_MOUSE_WHEEL_UP = 104
|
||||
## Mouse wheel down
|
||||
ACTION_MOUSE_WHEEL_DOWN = 105
|
||||
## Mouse drag
|
||||
ACTION_MOUSE_DRAG = 106
|
||||
## Mouse move
|
||||
ACTION_MOUSE_MOVE = 107
|
||||
|
||||
|
||||
def _set_textures(textures={}, kwargs={}):
|
||||
"""Set texture arguments for controls."""
|
||||
for texture in textures.keys():
|
||||
try:
|
||||
kwargs[texture]
|
||||
except KeyError:
|
||||
kwargs[texture] = textures[texture]
|
||||
|
||||
|
||||
class AddonWindowError(Exception):
|
||||
"""Custom exception."""
|
||||
pass
|
||||
|
||||
|
||||
class Label(xbmcgui.ControlLabel):
|
||||
"""ControlLabel class.
|
||||
|
||||
Parameters:
|
||||
label: string or unicode - text string.
|
||||
font: string - font used for label text. (e.g. 'font13')
|
||||
textColor: hexstring - color of enabled label's label. (e.g. '0xFFFFFFFF')
|
||||
disabledColor: hexstring - color of disabled label's label. (e.g. '0xFFFF3300')
|
||||
alignment: integer - alignment of label - *Note, see xbfont.h
|
||||
hasPath: bool - True=stores a path / False=no path.
|
||||
angle: integer - angle of control. (+ rotates CCW, - rotates CW)"
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.label = Label('Status', angle=45)
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super(Label, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class FadeLabel(xbmcgui.ControlFadeLabel):
|
||||
"""Control that scrolls label text.
|
||||
|
||||
Parameters:
|
||||
font: string - font used for label text. (e.g. 'font13')
|
||||
textColor: hexstring - color of fadelabel's labels. (e.g. '0xFFFFFFFF')
|
||||
_alignment: integer - alignment of label - *Note, see xbfont.h
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.fadelabel = FadeLabel(textColor='0xFFFFFFFF')
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super(FadeLabel, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class TextBox(xbmcgui.ControlTextBox):
|
||||
"""ControlTextBox class.
|
||||
|
||||
Parameters:
|
||||
font: string - font used for text. (e.g. 'font13')
|
||||
textColor: hexstring - color of textbox's text. (e.g. '0xFFFFFFFF')
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.textbox = TextBox(textColor='0xFFFFFFFF')
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super(TextBox, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class Image(xbmcgui.ControlImage):
|
||||
"""ControlImage class.
|
||||
|
||||
Parameters:
|
||||
filename: string - image filename.
|
||||
colorKey: hexString - (example, '0xFFFF3300')
|
||||
aspectRatio: integer - (values 0 = stretch (default), 1 = scale up (crops), 2 = scale down (black bars)
|
||||
colorDiffuse: hexString - (example, '0xC0FF0000' (red tint)).
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.image = Image('d:\images\picture.jpg', aspectRatio=2)
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super(Image, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class Button(xbmcgui.ControlButton):
|
||||
"""ControlButton class.
|
||||
|
||||
Parameters:
|
||||
label: string or unicode - text string.
|
||||
focusTexture: string - filename for focus texture.
|
||||
noFocusTexture: string - filename for no focus texture.
|
||||
textOffsetX: integer - x offset of label.
|
||||
textOffsetY: integer - y offset of label.
|
||||
alignment: integer - alignment of label - *Note, see xbfont.h
|
||||
font: string - font used for label text. (e.g. 'font13')
|
||||
textColor: hexstring - color of enabled button's label. (e.g. '0xFFFFFFFF')
|
||||
disabledColor: hexstring - color of disabled button's label. (e.g. '0xFFFF3300')
|
||||
angle: integer - angle of control. (+ rotates CCW, - rotates CW)
|
||||
shadowColor: hexstring - color of button's label's shadow. (e.g. '0xFF000000')
|
||||
focusedColor: hexstring - color of focused button's label. (e.g. '0xFF00FFFF')
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.button = Button('Status', font='font14')
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
textures = {'focusTexture': os.path.join(_images, 'Button', 'KeyboardKey.png'),
|
||||
'noFocusTexture': os.path.join(_images, 'Button', 'KeyboardKeyNF.png')}
|
||||
_set_textures(textures, kwargs)
|
||||
try:
|
||||
kwargs['alignment']
|
||||
except KeyError:
|
||||
kwargs['alignment'] = ALIGN_CENTER
|
||||
return super(Button, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class RadioButton(xbmcgui.ControlRadioButton):
|
||||
"""ControlRadioButton class.
|
||||
|
||||
Parameters:
|
||||
label: string or unicode - text string.
|
||||
focusTexture: string - filename for focus texture.
|
||||
noFocusTexture: string - filename for no focus texture.
|
||||
textOffsetX: integer - x offset of label.
|
||||
textOffsetY: integer - y offset of label.
|
||||
_alignment: integer - alignment of label - *Note, see xbfont.h
|
||||
font: string - font used for label text. (e.g. 'font13')
|
||||
textColor: hexstring - color of enabled radio button's label. (e.g. '0xFFFFFFFF')
|
||||
disabledColor: hexstring - color of disabled radio button's label. (e.g. '0xFFFF3300')
|
||||
angle: integer - angle of control. (+ rotates CCW, - rotates CW)
|
||||
shadowColor: hexstring - color of radio button's label's shadow. (e.g. '0xFF000000')
|
||||
focusedColor: hexstring - color of focused radio button's label. (e.g. '0xFF00FFFF')
|
||||
focusOnTexture: string - filename for radio focused/checked texture.
|
||||
noFocusOnTexture: string - filename for radio not focused/checked texture.
|
||||
focusOffTexture: string - filename for radio focused/unchecked texture.
|
||||
noFocusOffTexture: string - filename for radio not focused/unchecked texture.
|
||||
Note: To customize RadioButton all 4 abovementioned textures need to be provided.
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.radiobutton = RadioButton('Status', font='font14')
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 13:
|
||||
textures = {'focusTexture': os.path.join(_images, 'RadioButton', 'MenuItemFO.png'),
|
||||
'noFocusTexture': os.path.join(_images, 'RadioButton', 'MenuItemNF.png'),
|
||||
'focusOnTexture': os.path.join(_images, 'RadioButton', 'radiobutton-focus.png'),
|
||||
'noFocusOnTexture': os.path.join(_images, 'RadioButton', 'radiobutton-focus.png'),
|
||||
'focusOffTexture': os.path.join(_images, 'RadioButton', 'radiobutton-nofocus.png'),
|
||||
'noFocusOffTexture': os.path.join(_images, 'RadioButton', 'radiobutton-nofocus.png')}
|
||||
else: # This is for compatibility with Frodo and earlier versions.
|
||||
textures = {'focusTexture': os.path.join(_images, 'RadioButton', 'MenuItemFO.png'),
|
||||
'noFocusTexture': os.path.join(_images, 'RadioButton', 'MenuItemNF.png'),
|
||||
'TextureRadioFocus': os.path.join(_images, 'RadioButton', 'radiobutton-focus.png'),
|
||||
'TextureRadioNoFocus': os.path.join(_images, 'RadioButton', 'radiobutton-nofocus.png')}
|
||||
_set_textures(textures, kwargs)
|
||||
return super(RadioButton, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class Edit(xbmcgui.ControlEdit):
|
||||
"""
|
||||
ControlEdit class.
|
||||
|
||||
Edit(label[, font, textColor, disabledColor, alignment, focusTexture, noFocusTexture])
|
||||
|
||||
Parameters:
|
||||
label : string or unicode - text string.
|
||||
font : [opt] string - font used for label text. (e.g. 'font13')
|
||||
textColor : [opt] hexstring - color of enabled label's label. (e.g. '0xFFFFFFFF')
|
||||
disabledColor : [opt] hexstring - color of disabled label's label. (e.g. '0xFFFF3300')
|
||||
_alignment : [opt] integer - alignment of label - *Note, see xbfont.h
|
||||
focusTexture : [opt] string - filename for focus texture.
|
||||
noFocusTexture : [opt] string - filename for no focus texture.
|
||||
isPassword : [opt] bool - if true, mask text value.
|
||||
|
||||
*Note, You can use the above as keywords for arguments and skip certain optional arguments.
|
||||
Once you use a keyword, all following arguments require the keyword.
|
||||
After you create the control, you need to add it to the window with palceControl().
|
||||
|
||||
example:
|
||||
- self.edit = Edit('Status')
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
textures = {'focusTexture': os.path.join(_images, 'Edit', 'button-focus.png'),
|
||||
'noFocusTexture': os.path.join(_images, 'Edit', 'black-back2.png')}
|
||||
_set_textures(textures, kwargs)
|
||||
return super(Edit, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class List(xbmcgui.ControlList):
|
||||
"""ControlList class.
|
||||
|
||||
Parameters:
|
||||
font: string - font used for items label. (e.g. 'font13')
|
||||
textColor: hexstring - color of items label. (e.g. '0xFFFFFFFF')
|
||||
buttonTexture: string - filename for no focus texture.
|
||||
buttonFocusTexture: string - filename for focus texture.
|
||||
selectedColor: integer - x offset of label.
|
||||
_imageWidth: integer - width of items icon or thumbnail.
|
||||
_imageHeight: integer - height of items icon or thumbnail.
|
||||
_itemTextXOffset: integer - x offset of items label.
|
||||
_itemTextYOffset: integer - y offset of items label.
|
||||
_itemHeight: integer - height of items.
|
||||
_space: integer - space between items.
|
||||
_alignmentY: integer - Y-axis alignment of items label - *Note, see xbfont.h
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.cList = List('font14', space=5)
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
textures = {'buttonTexture': os.path.join(_images, 'List', 'MenuItemNF.png'),
|
||||
'buttonFocusTexture': os.path.join(_images, 'List', 'MenuItemFO.png')}
|
||||
_set_textures(textures, kwargs)
|
||||
return super(List, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class Slider(xbmcgui.ControlSlider):
|
||||
"""ControlSlider class.
|
||||
|
||||
Parameters:
|
||||
textureback: string - image filename.
|
||||
texture: string - image filename.
|
||||
texturefocus: string - image filename.
|
||||
|
||||
Note:
|
||||
After you create the control, you need to add it to the window with placeControl().
|
||||
|
||||
Example:
|
||||
self.slider = Slider()
|
||||
"""
|
||||
def __new__(cls, *args, **kwargs):
|
||||
textures = {'textureback': os.path.join(_images, 'Slider', 'osd_slider_bg.png'),
|
||||
'texture': os.path.join(_images, 'Slider', 'osd_slider_nibNF.png'),
|
||||
'texturefocus': os.path.join(_images, 'Slider', 'osd_slider_nib.png')}
|
||||
_set_textures(textures, kwargs)
|
||||
return super(Slider, cls).__new__(cls, -10, -10, 1, 1, *args, **kwargs)
|
||||
|
||||
|
||||
class _AbstractWindow(object):
|
||||
|
||||
"""
|
||||
Top-level control window.
|
||||
|
||||
The control windows serves as a parent widget for other XBMC UI controls
|
||||
much like Tkinter.Tk or PyQt QWidget class.
|
||||
This is an abstract class which is not supposed to be instantiated directly
|
||||
and will raise exeptions.
|
||||
|
||||
This class is a basic "skeleton" for a control window.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor method."""
|
||||
self.actions_connected = []
|
||||
self.controls_connected = []
|
||||
|
||||
def setGeometry(self, width_, height_, rows_, columns_, pos_x=-1, pos_y=-1):
|
||||
"""
|
||||
Set width, height, Grid layout, and coordinates (optional) for a new control window.
|
||||
|
||||
Parameters:
|
||||
width_, height_: widgh and height of the created window.
|
||||
rows_, columns_: rows and colums of the Grid layout to place controls on.
|
||||
pos_x, pos_y (optional): coordinates of the top left corner of the window.
|
||||
If pos_x and pos_y are not privided, the window will be placed
|
||||
at the center of the screen.
|
||||
Example:
|
||||
self.setGeometry(400, 500, 5, 4)
|
||||
"""
|
||||
self.width = width_
|
||||
self.height = height_
|
||||
self.rows = rows_
|
||||
self.columns = columns_
|
||||
if pos_x > 0 and pos_y > 0:
|
||||
self.x = pos_x
|
||||
self.y = pos_y
|
||||
else:
|
||||
self.x = 640 - self.width/2
|
||||
self.y = 360 - self.height/2
|
||||
self.setGrid()
|
||||
|
||||
def setGrid(self):
|
||||
"""
|
||||
Set window grid layout of rows * columns.
|
||||
This is a helper method not to be called directly.
|
||||
"""
|
||||
self.grid_x = self.x
|
||||
self.grid_y = self.y
|
||||
self.tile_width = self.width/self.columns
|
||||
self.tile_height = self.height/self.rows
|
||||
|
||||
def placeControl(self, control, row, column, rowspan=1, columnspan=1, pad_x=5, pad_y=5):
|
||||
"""
|
||||
Place a control within the window grid layout.
|
||||
|
||||
pad_x, pad_y: horisontal and vertical padding for control's
|
||||
size and aspect adjustments. Negative values can be used
|
||||
to make a control overlap with grid cells next to it, if necessary.
|
||||
Raises AddonWindowError if a grid has not yet been set.
|
||||
Example:
|
||||
self.placeControl(self.label, 0, 1)
|
||||
"""
|
||||
try:
|
||||
control_x = (self.grid_x + self.tile_width * column) + pad_x
|
||||
control_y = (self.grid_y + self.tile_height * row) + pad_y
|
||||
control_width = self.tile_width * columnspan - 2 * pad_x
|
||||
control_height = self.tile_height * rowspan - 2 * pad_y
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Window geometry is not defined! Call setGeometry first.')
|
||||
control.setPosition(control_x, control_y)
|
||||
control.setWidth(control_width)
|
||||
control.setHeight(control_height)
|
||||
self.addControl(control)
|
||||
self.setAnimation(control)
|
||||
|
||||
def getX(self):
|
||||
"""Get X coordinate of the top-left corner of the window."""
|
||||
try:
|
||||
return self.x
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Window geometry is not defined! Call setGeometry first.')
|
||||
|
||||
def getY(self):
|
||||
"""Get Y coordinate of the top-left corner of the window."""
|
||||
try:
|
||||
return self.y
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Window geometry is not defined! Call setGeometry first.')
|
||||
|
||||
def getWindowWidth(self):
|
||||
"""Get window width."""
|
||||
try:
|
||||
return self.width
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Window geometry is not defined! Call setGeometry first.')
|
||||
|
||||
def getWindowHeight(self):
|
||||
"""Get window height."""
|
||||
try:
|
||||
return self.height
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Window geometry is not defined! Call setGeometry first.')
|
||||
|
||||
def getRows(self):
|
||||
"""
|
||||
Get grid rows count.
|
||||
Raises AddonWindowError if a grid has not yet been set.
|
||||
"""
|
||||
try:
|
||||
return self.rows
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Grid layot is not set! Call setGeometry first.')
|
||||
|
||||
def getColumns(self):
|
||||
"""
|
||||
Get grid columns count.
|
||||
Raises AddonWindowError if a grid has not yet been set.
|
||||
"""
|
||||
try:
|
||||
return self.columns
|
||||
except AttributeError:
|
||||
raise AddonWindowError('Grid layout is not set! Call setGeometry first.')
|
||||
|
||||
def connect(self, event, function):
|
||||
"""
|
||||
Connect an event to a function.
|
||||
|
||||
An event can be an inctance of a Control object or an integer key action code.
|
||||
Several basic key action codes are provided by PyXBMCT. More action codes can be found at
|
||||
https://github.com/xbmc/xbmc/blob/master/xbmc/guilib/Key.h
|
||||
|
||||
You can connect the following Controls: Button, RadioButton and List. Other Controls do not
|
||||
generate any control events when activated so their connections won't work.
|
||||
To catch Slider events you need to connect the following key actions:
|
||||
ACTION_MOVE_LEFT, ACTION_MOVE_RIGHT and ACTION_MOUSE_DRAG, and do a check
|
||||
whether the Slider is focused.
|
||||
|
||||
"function" parameter is a function or a method to be executed. Note that you must provide
|
||||
a function object [without brackets ()], not a function call!
|
||||
lambda can be used as a function to call another function or method with parameters.
|
||||
|
||||
Examples:
|
||||
self.connect(self.exit_button, self.close)
|
||||
or
|
||||
self.connect(ACTION_NAV_BACK, self.close)
|
||||
"""
|
||||
try:
|
||||
self.disconnect(event)
|
||||
except AddonWindowError:
|
||||
if type(event) == int:
|
||||
self.actions_connected.append([event, function])
|
||||
else:
|
||||
self.controls_connected.append([event, function])
|
||||
|
||||
def connectEventList(self, events, function):
|
||||
"""
|
||||
Connect a list of controls/action codes to a function.
|
||||
See connect docstring for more info.
|
||||
"""
|
||||
[self.connect(event, function) for event in events]
|
||||
|
||||
def disconnect(self, event):
|
||||
"""
|
||||
Disconnect an event from a function.
|
||||
|
||||
An event can be an inctance of a Control object or an integer key action code
|
||||
which has previously been connected to a function or a method.
|
||||
Raises AddonWindowError if an event is not connected to any function.
|
||||
|
||||
Examples:
|
||||
self.disconnect(self.exit_button)
|
||||
or
|
||||
self.disconnect(ACTION_NAV_BACK)
|
||||
"""
|
||||
if type(event) == int:
|
||||
event_list = self.actions_connected
|
||||
else:
|
||||
event_list = self.controls_connected
|
||||
for index in range(len(event_list)):
|
||||
if event == event_list[index][0]:
|
||||
event_list.pop(index)
|
||||
break
|
||||
else:
|
||||
raise AddonWindowError('The action or control %s is not connected!' % event)
|
||||
|
||||
def disconnectEventList(self, events):
|
||||
"""
|
||||
Disconnect a list of controls/action codes from functions.
|
||||
See disconnect docstring for more info.
|
||||
Raises AddonWindowError if at least one event in the list
|
||||
is not connected to any function.
|
||||
"""
|
||||
[self.disconnect(event) for event in events]
|
||||
|
||||
def executeConnected(self, event, connected_list):
|
||||
"""
|
||||
Execute a connected event (an action or a control).
|
||||
This is a helper method not to be called directly.
|
||||
"""
|
||||
for item in connected_list:
|
||||
if event == item[0]:
|
||||
item[1]()
|
||||
break
|
||||
|
||||
def setAnimation(self, control):
|
||||
"""
|
||||
This method is called to set animation properties for all controls
|
||||
added to the current addon window instance - both built-in controls
|
||||
(window background, title bar etc.) and controls added with placeControl().
|
||||
It receives a control instance as the 2nd positional argument (besides self).
|
||||
By default the method does nothing, i.e. no animation is set for controls.
|
||||
To add animation you need to re-implement this menthod in your child class.
|
||||
|
||||
E.g:
|
||||
def setAnimation(self, control):
|
||||
control.setAnimations([('WindowOpen', 'effect=fade start=0 end=100 time=1000',),
|
||||
('WindowClose', 'effect=fade start=100 end=0 time=1000',)])
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _AddonWindow(_AbstractWindow):
|
||||
|
||||
"""
|
||||
Top-level control window.
|
||||
|
||||
The control windows serves as a parent widget for other XBMC UI controls
|
||||
much like Tkinter.Tk or PyQt QWidget class.
|
||||
This is an abstract class which is not supposed to be instantiated directly
|
||||
and will raise exeptions. It is designed to be implemented in a grand-child class
|
||||
with the second inheritance from xbmcgui.Window or xbmcgui.WindowDialog
|
||||
in a direct child class.
|
||||
|
||||
This class provides a control window with a background and a header
|
||||
similar to top-level widgets of desktop UI frameworks.
|
||||
"""
|
||||
|
||||
def __init__(self, title=''):
|
||||
"""Constructor method."""
|
||||
super(_AddonWindow, self).__init__()
|
||||
self.setFrame(title)
|
||||
|
||||
def setFrame(self, title):
|
||||
"""
|
||||
Define paths to images for window background and title background textures,
|
||||
and set control position adjustment constants used in setGrid.
|
||||
This is a helper method not to be called directly.
|
||||
"""
|
||||
# Window background image
|
||||
self.background_img = os.path.join(_images, 'AddonWindow', 'ContentPanel.png')
|
||||
# Background for a window header
|
||||
self.title_background_img = os.path.join(_images, 'AddonWindow', 'dialogheader.png')
|
||||
# Horisontal adjustment for a header background if the main background has transparent edges.
|
||||
self.X_MARGIN = 5
|
||||
# Vertical adjustment for a header background if the main background has transparent edges
|
||||
self.Y_MARGIN = 5
|
||||
# Header position adjustment if the main backround has visible borders.
|
||||
self.Y_SHIFT = 4
|
||||
# The height of a window header (for the title background and the title label).
|
||||
self.HEADER_HEIGHT = 35
|
||||
self.background = xbmcgui.ControlImage(-10, -10, 1, 1, self.background_img)
|
||||
self.addControl(self.background)
|
||||
self.setAnimation(self.background)
|
||||
self.title_background = xbmcgui.ControlImage(-10, -10, 1, 1, self.title_background_img)
|
||||
self.addControl(self.title_background)
|
||||
self.setAnimation(self.title_background)
|
||||
self.title_bar = xbmcgui.ControlLabel(-10, -10, 1, 1, title, alignment=ALIGN_CENTER, textColor='0xFFFFA500',
|
||||
font='font13_title')
|
||||
self.addControl(self.title_bar)
|
||||
self.setAnimation(self.title_bar)
|
||||
self.window_close_button = xbmcgui.ControlButton(-100, -100, 60, 30, '',
|
||||
focusTexture=os.path.join(_images, 'AddonWindow', 'DialogCloseButton-focus.png'),
|
||||
noFocusTexture=os.path.join(_images, 'AddonWindow', 'DialogCloseButton.png'))
|
||||
self.addControl(self.window_close_button)
|
||||
self.setAnimation(self.window_close_button)
|
||||
|
||||
def setGeometry(self, width_, height_, rows_, columns_, pos_x=-1, pos_y=-1, padding=5):
|
||||
"""
|
||||
Set width, height, Grid layout, and coordinates (optional) for a new control window.
|
||||
|
||||
Parameters:
|
||||
width_, height_: widgh and height of the created window.
|
||||
rows_, columns_: rows and colums of the Grid layout to place controls on.
|
||||
pos_x, pos_y (optional): coordinates of the top left corner of the window.
|
||||
If pos_x and pos_y are not privided, the window will be placed
|
||||
at the center of the screen.
|
||||
padding (optional): padding between outer edges of the window and
|
||||
controls placed on it.
|
||||
Example:
|
||||
self.setGeometry(400, 500, 5, 4)
|
||||
"""
|
||||
self.win_padding = padding
|
||||
super(_AddonWindow, self).setGeometry(width_, height_, rows_, columns_, pos_x, pos_y)
|
||||
self.background.setPosition(self.x, self.y)
|
||||
self.background.setWidth(self.width)
|
||||
self.background.setHeight(self.height)
|
||||
self.title_background.setPosition(self.x + self.X_MARGIN, self.y + self.Y_MARGIN + self.Y_SHIFT)
|
||||
self.title_background.setWidth(self.width - 2 * self.X_MARGIN)
|
||||
self.title_background.setHeight(self.HEADER_HEIGHT)
|
||||
self.title_bar.setPosition(self.x + self.X_MARGIN, self.y + self.Y_MARGIN + self.Y_SHIFT)
|
||||
self.title_bar.setWidth(self.width - 2 * self.X_MARGIN)
|
||||
self.title_bar.setHeight(self.HEADER_HEIGHT)
|
||||
self.window_close_button.setPosition(self.x + self.width - 70, self.y + self.Y_MARGIN + self.Y_SHIFT)
|
||||
|
||||
def setGrid(self):
|
||||
"""
|
||||
Set window grid layout of rows * columns.
|
||||
This is a helper method not to be called directly.
|
||||
"""
|
||||
self.grid_x = self.x + self.X_MARGIN + self.win_padding
|
||||
self.grid_y = self.y + self.Y_MARGIN + self.Y_SHIFT + self.HEADER_HEIGHT + self.win_padding
|
||||
self.tile_width = (self.width - 2 * (self.X_MARGIN + self.win_padding))/self.columns
|
||||
self.tile_height = (
|
||||
self.height - self.HEADER_HEIGHT - self.Y_SHIFT - 2 * (self.Y_MARGIN + self.win_padding))/self.rows
|
||||
|
||||
def setWindowTitle(self, title=''):
|
||||
"""
|
||||
Set window title.
|
||||
This method must be called AFTER (!!!) setGeometry(),
|
||||
otherwise there is some werid bug with all skin text labels set to the 'title' text.
|
||||
Example:
|
||||
self.setWindowTitle('My Cool Addon')
|
||||
"""
|
||||
self.title_bar.setLabel(title)
|
||||
|
||||
def getWindowTitle(self):
|
||||
"""Get window title."""
|
||||
return self.title_bar.getLabel()
|
||||
|
||||
class _FullWindow(xbmcgui.Window):
|
||||
|
||||
"""An abstract class to define window event processing."""
|
||||
|
||||
def onAction(self, action):
|
||||
"""
|
||||
Catch button actions.
|
||||
Note that, despite being compared to an integer,
|
||||
action is an instance of xbmcgui.Action class.
|
||||
"""
|
||||
if action == ACTION_PREVIOUS_MENU:
|
||||
self.close()
|
||||
else:
|
||||
self.executeConnected(action, self.actions_connected)
|
||||
|
||||
def onControl(self, control):
|
||||
"""
|
||||
Catch activated controls.
|
||||
Control is an instance of xbmcgui.Control class.
|
||||
"""
|
||||
if control == self.window_close_button:
|
||||
self.close()
|
||||
else:
|
||||
self.executeConnected(control, self.controls_connected)
|
||||
|
||||
|
||||
class _DialogWindow(xbmcgui.WindowDialog):
|
||||
|
||||
"""An abstract class to define window event processing."""
|
||||
|
||||
def onAction(self, action):
|
||||
"""
|
||||
Catch button actions.
|
||||
Note that, despite being compared to an integer,
|
||||
action is an instance of xbmcgui.Action class.
|
||||
"""
|
||||
if action == ACTION_PREVIOUS_MENU:
|
||||
self.close()
|
||||
else:
|
||||
self.executeConnected(action, self.actions_connected)
|
||||
|
||||
def onControl(self, control):
|
||||
"""
|
||||
Catch activated controls.
|
||||
Control is an instance of xbmcgui.Control class.
|
||||
"""
|
||||
if control == self.window_close_button:
|
||||
self.close()
|
||||
else:
|
||||
self.executeConnected(control, self.controls_connected)
|
||||
|
||||
|
||||
class BlankFullWindow(_FullWindow, _AbstractWindow):
|
||||
"""
|
||||
Addon UI container with a solid background.
|
||||
This is a blank window with a black background and without any elements whatsoever.
|
||||
The decoration and layout are completely up to an addon developer.
|
||||
The window controls can hide under video or music visualization.
|
||||
Window ID can be passed on class instantiation an agrument
|
||||
but __init__ must have the 2nd fake argument, e.g:
|
||||
|
||||
def __init__(self, *args)
|
||||
|
||||
Minimal example:
|
||||
|
||||
addon = MyAddon('My Cool Addon')
|
||||
addon.setGeometry(400, 300, 4, 3)
|
||||
addon.doModal()
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BlankDialogWindow(_DialogWindow, _AbstractWindow):
|
||||
"""
|
||||
Addon UI container with a transparent background.
|
||||
This is a blank window with a transparent background and without any elements whatsoever.
|
||||
The decoration and layout are completely up to an addon developer.
|
||||
The window controls are always displayed over video or music visualization.
|
||||
Minimal example:
|
||||
|
||||
addon = MyAddon('My Cool Addon')
|
||||
addon.setGeometry(400, 300, 4, 3)
|
||||
addon.doModal()
|
||||
"""
|
||||
pass
|
||||
|
||||
class AddonFullWindow(_FullWindow, _AddonWindow):
|
||||
|
||||
"""
|
||||
Addon UI container with a solid background.
|
||||
Control window is displayed on top of the main background image - self.main_bg.
|
||||
Video and music visualization are displayed unhindered.
|
||||
Window ID can be passed on class instantiation as the 2nd positional agrument
|
||||
but __init__ must have the 3rd fake argument, e.g:
|
||||
|
||||
def __init__(self, title='', *args)
|
||||
|
||||
Minimal example:
|
||||
|
||||
addon = MyAddon('My Cool Addon')
|
||||
addon.setGeometry(400, 300, 4, 3)
|
||||
addon.doModal()
|
||||
"""
|
||||
|
||||
def __new__(cls, title='', *args, **kwargs):
|
||||
return super(AddonFullWindow, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
def setFrame(self, title):
|
||||
"""
|
||||
Set the image for for the fullscreen background.
|
||||
"""
|
||||
# Image for the fullscreen background.
|
||||
self.main_bg_img = os.path.join(_images, 'AddonWindow', 'SKINDEFAULT.jpg')
|
||||
# Fullscreen background image control.
|
||||
self.main_bg = xbmcgui.ControlImage(1, 1, 1280, 720, self.main_bg_img)
|
||||
self.addControl(self.main_bg)
|
||||
super(AddonFullWindow, self).setFrame(title)
|
||||
|
||||
def setBackground(self, image=''):
|
||||
"""
|
||||
Set the main bacground to an image file.
|
||||
image: path to an image file as str.
|
||||
Example:
|
||||
self.setBackground('d:\images\bacground.png')
|
||||
"""
|
||||
self.main_bg.setImage(image)
|
||||
|
||||
|
||||
class AddonDialogWindow(_DialogWindow, _AddonWindow):
|
||||
"""
|
||||
Addon UI container with a transparent background.
|
||||
Control window is displayed on top of XBMC UI,
|
||||
including video an music visualization!
|
||||
Minimal example:
|
||||
|
||||
addon = MyAddon('My Cool Addon')
|
||||
addon.setGeometry(400, 300, 4, 3)
|
||||
addon.doModal()
|
||||
"""
|
||||
pass
|
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import time
|
||||
import pickle
|
||||
import threading
|
||||
import zipfile
|
||||
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
import Localization
|
||||
from net import HTTP
|
||||
|
||||
|
||||
try:
|
||||
from sqlite3 import dbapi2 as sqlite
|
||||
except:
|
||||
from pysqlite2 import dbapi2 as sqlite
|
||||
|
||||
rtrCache_lock = threading.RLock()
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(self, name, version, expire=0, size=0, step=100):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self._connect()
|
||||
if expire:
|
||||
self.expire(expire)
|
||||
if size:
|
||||
self.size(size, step)
|
||||
|
||||
def get(self, token, callback, *param):
|
||||
cur = self.db.cursor()
|
||||
cur.execute('select expire,data from cache where id=? limit 1', (token, ))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
if row:
|
||||
if row[0] and row[0] < int(time.time()):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
obj = pickle.loads(row[1])
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
return obj
|
||||
|
||||
response = callback(*param)
|
||||
|
||||
if response[0]:
|
||||
obj = sqlite.Binary(pickle.dumps(response[1]))
|
||||
curtime = int(time.time())
|
||||
cur = self.db.cursor()
|
||||
if isinstance(response[0], bool):
|
||||
cur.execute('replace into cache(id,addtime,expire,data) values(?,?,?,?)', (token, curtime, None, obj))
|
||||
else:
|
||||
cur.execute('replace into cache(id,addtime,expire,data) values(?,?,?,?)',
|
||||
(token, curtime, curtime + response[0], obj))
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
return response[1]
|
||||
|
||||
def expire(self, expire):
|
||||
# with rtrCache_lock:
|
||||
cur = self.db.cursor()
|
||||
cur.execute('delete from cache where addtime<?', (int(time.time()) - expire, ))
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
def size(self, size, step=100):
|
||||
# with rtrCache_lock:
|
||||
while True:
|
||||
if os.path.getsize(self.filename) < size:
|
||||
break
|
||||
cur = self.db.cursor()
|
||||
cur.execute('select id from cache order by addtime asc limit ?', (step, ))
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
cur.close()
|
||||
break
|
||||
cur.execute('delete from cache where id in (' + ','.join(len(rows) * '?') + ')', [x[0] for x in rows])
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
def flush(self):
|
||||
# with rtrCache_lock:
|
||||
cur = self.db.cursor()
|
||||
cur.execute('delete from cache')
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
def _connect(self):
|
||||
with rtrCache_lock:
|
||||
dirname = xbmc.translatePath('special://temp')
|
||||
for subdir in ('xbmcup', 'plugin.video.torrenter'):
|
||||
dirname = os.path.join(dirname, subdir)
|
||||
if not xbmcvfs.exists(dirname):
|
||||
xbmcvfs.mkdir(dirname)
|
||||
|
||||
self.filename = os.path.join(dirname, self.name)
|
||||
|
||||
first = False
|
||||
if not xbmcvfs.exists(self.filename):
|
||||
first = True
|
||||
|
||||
self.db = sqlite.connect(self.filename, check_same_thread=False)
|
||||
if not first:
|
||||
cur = self.db.cursor()
|
||||
try:
|
||||
cur.execute('select version from db_ver')
|
||||
row = cur.fetchone()
|
||||
if not row or float(row[0]) != self.version:
|
||||
cur.execute('drop table cache')
|
||||
cur.execute('drop table if exists db_ver')
|
||||
first = True
|
||||
except:
|
||||
cur.execute('drop table cache')
|
||||
first = True
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
if first and not self.first_time():
|
||||
cur = self.db.cursor()
|
||||
cur.execute('pragma auto_vacuum=1')
|
||||
cur.execute('create table cache(id varchar(255) unique, addtime integer, expire integer, data blob)')
|
||||
cur.execute('create index time on cache(addtime asc)')
|
||||
cur.execute('create table db_ver(version real)')
|
||||
cur.execute('insert into db_ver(version) values(?)', (self.version, ))
|
||||
self.db.commit()
|
||||
cur.close()
|
||||
|
||||
def first_time(self):
|
||||
scrapers = {'tvdb': 'TheTVDB.com', 'tmdb': 'TheMovieDB.org', 'kinopoisk': 'KinoPoisk.ru'}
|
||||
ok = xbmcgui.Dialog().yesno(Localization.localize('Content Lists'),
|
||||
Localization.localize('Do you want to preload full metadata?') + ' (%s)' % (
|
||||
scrapers[os.path.basename(self.filename).split('.')[0]]),
|
||||
Localization.localize('It is highly recommended!'))
|
||||
if ok:
|
||||
return self.download()
|
||||
else:
|
||||
return False
|
||||
|
||||
def download(self):
|
||||
dirname = os.path.dirname(self.filename)
|
||||
print self.filename
|
||||
zipname = os.path.basename(self.filename).rstrip('.db') + '.zip'
|
||||
url = 'http://www.tat-store.ru/torrenter/' + zipname
|
||||
self.http = HTTP()
|
||||
response = self.http.fetch(url, download=os.path.join(dirname, zipname), progress=True)
|
||||
if response.error:
|
||||
return False
|
||||
|
||||
try:
|
||||
filezip = zipfile.ZipFile(os.path.join(dirname, zipname), 'r')
|
||||
filezip.extractall(dirname)
|
||||
filezip.close()
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
"""
|
||||
StringMatcher.py
|
||||
|
||||
ported from python-Levenshtein
|
||||
[https://github.com/miohtama/python-Levenshtein]
|
||||
"""
|
||||
|
||||
from Levenshtein import *
|
||||
from warnings import warn
|
||||
|
||||
class StringMatcher:
|
||||
"""A SequenceMatcher-like class built on the top of Levenshtein"""
|
||||
|
||||
def _reset_cache(self):
|
||||
self._ratio = self._distance = None
|
||||
self._opcodes = self._editops = self._matching_blocks = None
|
||||
|
||||
def __init__(self, isjunk=None, seq1='', seq2=''):
|
||||
if isjunk:
|
||||
warn("isjunk not NOT implemented, it will be ignored")
|
||||
self._str1, self._str2 = seq1, seq2
|
||||
self._reset_cache()
|
||||
|
||||
def set_seqs(self, seq1, seq2):
|
||||
self._str1, self._str2 = seq1, seq2
|
||||
self._reset_cache()
|
||||
|
||||
def set_seq1(self, seq1):
|
||||
self._str1 = seq1
|
||||
self._reset_cache()
|
||||
|
||||
def set_seq2(self, seq2):
|
||||
self._str2 = seq2
|
||||
self._reset_cache()
|
||||
|
||||
def get_opcodes(self):
|
||||
if not self._opcodes:
|
||||
if self._editops:
|
||||
self._opcodes = opcodes(self._editops, self._str1, self._str2)
|
||||
else:
|
||||
self._opcodes = opcodes(self._str1, self._str2)
|
||||
return self._opcodes
|
||||
|
||||
def get_editops(self):
|
||||
if not self._editops:
|
||||
if self._opcodes:
|
||||
self._editops = editops(self._opcodes, self._str1, self._str2)
|
||||
else:
|
||||
self._editops = editops(self._str1, self._str2)
|
||||
return self._editops
|
||||
|
||||
def get_matching_blocks(self):
|
||||
if not self._matching_blocks:
|
||||
self._matching_blocks = matching_blocks(self.get_opcodes(),
|
||||
self._str1, self._str2)
|
||||
return self._matching_blocks
|
||||
|
||||
def ratio(self):
|
||||
if not self._ratio:
|
||||
self._ratio = ratio(self._str1, self._str2)
|
||||
return self._ratio
|
||||
|
||||
def quick_ratio(self):
|
||||
# This is usually quick enough :o)
|
||||
if not self._ratio:
|
||||
self._ratio = ratio(self._str1, self._str2)
|
||||
return self._ratio
|
||||
|
||||
def real_quick_ratio(self):
|
||||
len1, len2 = len(self._str1), len(self._str2)
|
||||
return 2.0 * min(len1, len2) / (len1 + len2)
|
||||
|
||||
def distance(self):
|
||||
if not self._distance:
|
||||
self._distance = distance(self._str1, self._str2)
|
||||
return self._distance
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__version__ = '0.4.0'
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
"""
|
||||
fuzz.py
|
||||
|
||||
Copyright (c) 2011 Adam Cohen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import warnings
|
||||
|
||||
try:
|
||||
from .StringMatcher import StringMatcher as SequenceMatcher
|
||||
except ImportError:
|
||||
#warnings.warn('Using slow pure-python SequenceMatcher. Install python-Levenshtein to remove this warning')
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
###########################
|
||||
# Basic Scoring Functions #
|
||||
###########################
|
||||
|
||||
|
||||
def ratio(s1, s2):
|
||||
|
||||
if s1 is None:
|
||||
raise TypeError("s1 is None")
|
||||
if s2 is None:
|
||||
raise TypeError("s2 is None")
|
||||
s1, s2 = utils.make_type_consistent(s1, s2)
|
||||
if len(s1) == 0 or len(s2) == 0:
|
||||
return 0
|
||||
|
||||
m = SequenceMatcher(None, s1, s2)
|
||||
return utils.intr(100 * m.ratio())
|
||||
|
||||
|
||||
# todo: skip duplicate indexes for a little more speed
|
||||
def partial_ratio(s1, s2):
|
||||
|
||||
if s1 is None:
|
||||
raise TypeError("s1 is None")
|
||||
if s2 is None:
|
||||
raise TypeError("s2 is None")
|
||||
s1, s2 = utils.make_type_consistent(s1, s2)
|
||||
if len(s1) == 0 or len(s2) == 0:
|
||||
return 0
|
||||
|
||||
if len(s1) <= len(s2):
|
||||
shorter = s1
|
||||
longer = s2
|
||||
else:
|
||||
shorter = s2
|
||||
longer = s1
|
||||
|
||||
m = SequenceMatcher(None, shorter, longer)
|
||||
blocks = m.get_matching_blocks()
|
||||
|
||||
# each block represents a sequence of matching characters in a string
|
||||
# of the form (idx_1, idx_2, len)
|
||||
# the best partial match will block align with at least one of those blocks
|
||||
# e.g. shorter = "abcd", longer = XXXbcdeEEE
|
||||
# block = (1,3,3)
|
||||
# best score === ratio("abcd", "Xbcd")
|
||||
scores = []
|
||||
for block in blocks:
|
||||
long_start = block[1] - block[0] if (block[1] - block[0]) > 0 else 0
|
||||
long_end = long_start + len(shorter)
|
||||
long_substr = longer[long_start:long_end]
|
||||
|
||||
m2 = SequenceMatcher(None, shorter, long_substr)
|
||||
r = m2.ratio()
|
||||
if r > .995:
|
||||
return 100
|
||||
else:
|
||||
scores.append(r)
|
||||
|
||||
return int(100 * max(scores))
|
||||
|
||||
|
||||
##############################
|
||||
# Advanced Scoring Functions #
|
||||
##############################
|
||||
|
||||
# Sorted Token
|
||||
# find all alphanumeric tokens in the string
|
||||
# sort those tokens and take ratio of resulting joined strings
|
||||
# controls for unordered string elements
|
||||
def _token_sort(s1, s2, partial=True, force_ascii=True):
|
||||
|
||||
if s1 is None:
|
||||
raise TypeError("s1 is None")
|
||||
if s2 is None:
|
||||
raise TypeError("s2 is None")
|
||||
|
||||
# pull tokens
|
||||
tokens1 = utils.full_process(s1, force_ascii=force_ascii).split()
|
||||
tokens2 = utils.full_process(s2, force_ascii=force_ascii).split()
|
||||
|
||||
# sort tokens and join
|
||||
sorted1 = " ".join(sorted(tokens1))
|
||||
sorted2 = " ".join(sorted(tokens2))
|
||||
|
||||
sorted1 = sorted1.strip()
|
||||
sorted2 = sorted2.strip()
|
||||
|
||||
if partial:
|
||||
return partial_ratio(sorted1, sorted2)
|
||||
else:
|
||||
return ratio(sorted1, sorted2)
|
||||
|
||||
|
||||
def token_sort_ratio(s1, s2, force_ascii=True):
|
||||
return _token_sort(s1, s2, partial=False, force_ascii=force_ascii)
|
||||
|
||||
|
||||
def partial_token_sort_ratio(s1, s2, force_ascii=True):
|
||||
return _token_sort(s1, s2, partial=True, force_ascii=force_ascii)
|
||||
|
||||
|
||||
# Token Set
|
||||
# find all alphanumeric tokens in each string...treat them as a set
|
||||
# construct two strings of the form
|
||||
# <sorted_intersection><sorted_remainder>
|
||||
# take ratios of those two strings
|
||||
# controls for unordered partial matches
|
||||
def _token_set(s1, s2, partial=True, force_ascii=True):
|
||||
|
||||
if s1 is None:
|
||||
raise TypeError("s1 is None")
|
||||
if s2 is None:
|
||||
raise TypeError("s2 is None")
|
||||
|
||||
p1 = utils.full_process(s1, force_ascii=force_ascii)
|
||||
p2 = utils.full_process(s2, force_ascii=force_ascii)
|
||||
|
||||
if not utils.validate_string(p1):
|
||||
return 0
|
||||
if not utils.validate_string(p2):
|
||||
return 0
|
||||
|
||||
# pull tokens
|
||||
tokens1 = set(utils.full_process(p1).split())
|
||||
tokens2 = set(utils.full_process(p2).split())
|
||||
|
||||
intersection = tokens1.intersection(tokens2)
|
||||
diff1to2 = tokens1.difference(tokens2)
|
||||
diff2to1 = tokens2.difference(tokens1)
|
||||
|
||||
sorted_sect = " ".join(sorted(intersection))
|
||||
sorted_1to2 = " ".join(sorted(diff1to2))
|
||||
sorted_2to1 = " ".join(sorted(diff2to1))
|
||||
|
||||
combined_1to2 = sorted_sect + " " + sorted_1to2
|
||||
combined_2to1 = sorted_sect + " " + sorted_2to1
|
||||
|
||||
# strip
|
||||
sorted_sect = sorted_sect.strip()
|
||||
combined_1to2 = combined_1to2.strip()
|
||||
combined_2to1 = combined_2to1.strip()
|
||||
|
||||
if partial:
|
||||
ratio_func = partial_ratio
|
||||
else:
|
||||
ratio_func = ratio
|
||||
|
||||
pairwise = [
|
||||
ratio_func(sorted_sect, combined_1to2),
|
||||
ratio_func(sorted_sect, combined_2to1),
|
||||
ratio_func(combined_1to2, combined_2to1)
|
||||
]
|
||||
return max(pairwise)
|
||||
|
||||
|
||||
def token_set_ratio(s1, s2, force_ascii=True):
|
||||
return _token_set(s1, s2, partial=False, force_ascii=force_ascii)
|
||||
|
||||
|
||||
def partial_token_set_ratio(s1, s2, force_ascii=True):
|
||||
return _token_set(s1, s2, partial=True, force_ascii=force_ascii)
|
||||
|
||||
|
||||
# TODO: numerics
|
||||
|
||||
###################
|
||||
# Combination API #
|
||||
###################
|
||||
|
||||
# q is for quick
|
||||
def QRatio(s1, s2, force_ascii=True):
|
||||
|
||||
p1 = utils.full_process(s1, force_ascii=force_ascii)
|
||||
p2 = utils.full_process(s2, force_ascii=force_ascii)
|
||||
|
||||
if not utils.validate_string(p1):
|
||||
return 0
|
||||
if not utils.validate_string(p2):
|
||||
return 0
|
||||
|
||||
return ratio(p1, p2)
|
||||
|
||||
|
||||
def UQRatio(s1, s2):
|
||||
return QRatio(s1, s2, force_ascii=False)
|
||||
|
||||
|
||||
# w is for weighted
|
||||
def WRatio(s1, s2, force_ascii=True):
|
||||
|
||||
p1 = utils.full_process(s1, force_ascii=force_ascii)
|
||||
p2 = utils.full_process(s2, force_ascii=force_ascii)
|
||||
|
||||
if not utils.validate_string(p1):
|
||||
return 0
|
||||
if not utils.validate_string(p2):
|
||||
return 0
|
||||
|
||||
# should we look at partials?
|
||||
try_partial = True
|
||||
unbase_scale = .95
|
||||
partial_scale = .90
|
||||
|
||||
base = ratio(p1, p2)
|
||||
len_ratio = float(max(len(p1), len(p2))) / min(len(p1), len(p2))
|
||||
|
||||
# if strings are similar length, don't use partials
|
||||
if len_ratio < 1.5:
|
||||
try_partial = False
|
||||
|
||||
# if one string is much much shorter than the other
|
||||
if len_ratio > 8:
|
||||
partial_scale = .6
|
||||
|
||||
if try_partial:
|
||||
partial = partial_ratio(p1, p2) * partial_scale
|
||||
ptsor = partial_token_sort_ratio(p1, p2, force_ascii=force_ascii) \
|
||||
* unbase_scale * partial_scale
|
||||
ptser = partial_token_set_ratio(p1, p2, force_ascii=force_ascii) \
|
||||
* unbase_scale * partial_scale
|
||||
|
||||
return int(max(base, partial, ptsor, ptser))
|
||||
else:
|
||||
tsor = token_sort_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale
|
||||
tser = token_set_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale
|
||||
|
||||
return int(max(base, tsor, tser))
|
||||
|
||||
|
||||
def UWRatio(s1, s2):
|
||||
return WRatio(s1, s2, force_ascii=False)
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
"""
|
||||
process.py
|
||||
|
||||
Copyright (c) 2011 Adam Cohen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
import itertools
|
||||
|
||||
from . import fuzz
|
||||
from . import utils
|
||||
|
||||
|
||||
def extract(query, choices, processor=None, scorer=None, limit=5):
|
||||
"""Find best matches in a list or dictionary of choices, return a
|
||||
list of tuples containing the match and it's score. If a dictionery
|
||||
is used, also returns the key for each match.
|
||||
|
||||
Arguments:
|
||||
query -- an object representing the thing we want to find
|
||||
choices -- a list or dictionary of objects we are attempting
|
||||
to extract values from. The dictionary should
|
||||
consist of {key: str} pairs.
|
||||
scorer -- f(OBJ, QUERY) --> INT. We will return the objects
|
||||
with the highest score by default, we use
|
||||
score.WRatio() and both OBJ and QUERY should be
|
||||
strings
|
||||
processor -- f(OBJ_A) --> OBJ_B, where the output is an input
|
||||
to scorer for example, "processor = lambda x:
|
||||
x[0]" would return the first element in a
|
||||
collection x (of, say, strings) this would then
|
||||
be used in the scoring collection by default, we
|
||||
use utils.full_process()
|
||||
|
||||
"""
|
||||
if choices is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
if len(choices) == 0:
|
||||
return []
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# default, turn whatever the choice is into a workable string
|
||||
if processor is None:
|
||||
processor = lambda x: utils.full_process(x)
|
||||
|
||||
# default: wratio
|
||||
if scorer is None:
|
||||
scorer = fuzz.WRatio
|
||||
|
||||
sl = list()
|
||||
|
||||
if isinstance(choices, dict):
|
||||
for key, choice in choices.items():
|
||||
processed = processor(choice)
|
||||
score = scorer(query, processed)
|
||||
tuple = (choice, score, key)
|
||||
sl.append(tuple)
|
||||
|
||||
else:
|
||||
for choice in choices:
|
||||
processed = processor(choice)
|
||||
score = scorer(query, processed)
|
||||
tuple = (choice, score)
|
||||
sl.append(tuple)
|
||||
|
||||
sl.sort(key=lambda i: i[1], reverse=True)
|
||||
return sl[:limit]
|
||||
|
||||
|
||||
def extractBests(query, choices, processor=None, scorer=None, score_cutoff=0, limit=5):
|
||||
"""Find best matches above a score in a list of choices, return a
|
||||
list of tuples containing the match and it's score.
|
||||
|
||||
Convenience method which returns the choices with best scores, see
|
||||
extract() for full arguments list
|
||||
|
||||
Optional parameter: score_cutoff.
|
||||
If the choice has a score of less than or equal to score_cutoff
|
||||
it will not be included on result list
|
||||
|
||||
"""
|
||||
|
||||
best_list = extract(query, choices, processor, scorer, limit)
|
||||
if len(best_list) > 0:
|
||||
return list(itertools.takewhile(lambda x: x[1] >= score_cutoff, best_list))
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def extractOne(query, choices, processor=None, scorer=None, score_cutoff=0):
|
||||
"""Find the best match above a score in a list of choices, return a
|
||||
tuple containing the match and it's score if it's above the treshold
|
||||
or None.
|
||||
|
||||
Convenience method which returns the single best choice, see
|
||||
extract() for full arguments list
|
||||
|
||||
Optional parameter: score_cutoff.
|
||||
If the best choice has a score of less than or equal to
|
||||
score_cutoff we will return none (intuition: not a good enough
|
||||
match)
|
||||
|
||||
"""
|
||||
|
||||
best_list = extract(query, choices, processor, scorer, limit=1)
|
||||
if len(best_list) > 0:
|
||||
best = best_list[0]
|
||||
if best[1] >= score_cutoff:
|
||||
return best
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
|
||||
|
||||
class StringProcessor(object):
|
||||
"""
|
||||
This class defines method to process strings in the most
|
||||
efficient way. Ideally all the methods below use unicode strings
|
||||
for both input and output.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def replace_non_letters_non_numbers_with_whitespace(cls, a_string):
|
||||
"""
|
||||
This function replaces any sequence of non letters and non
|
||||
numbers with a single white space.
|
||||
"""
|
||||
regex = re.compile(r"(?ui)\W")
|
||||
return regex.sub(" ", a_string)
|
||||
|
||||
@classmethod
|
||||
def strip(cls, a_string):
|
||||
"""
|
||||
This function strips leading and trailing white space.
|
||||
"""
|
||||
|
||||
return a_string.strip()
|
||||
|
||||
@classmethod
|
||||
def to_lower_case(cls, a_string):
|
||||
"""
|
||||
This function returns the lower-cased version of the string given.
|
||||
"""
|
||||
return a_string.lower()
|
||||
|
||||
@classmethod
|
||||
def to_upper_case(cls, a_string):
|
||||
"""
|
||||
This function returns the upper-cased version of the string given.
|
||||
"""
|
||||
return a_string.upper()
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
|
||||
from fuzzywuzzy.string_processing import StringProcessor
|
||||
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
def validate_string(s):
|
||||
try:
|
||||
if len(s) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
bad_chars = str('') # ascii dammit!
|
||||
for i in range(128, 256):
|
||||
bad_chars += chr(i)
|
||||
if PY3:
|
||||
translation_table = dict((ord(c), None) for c in bad_chars)
|
||||
|
||||
|
||||
def asciionly(s):
|
||||
if PY3:
|
||||
return s.translate(translation_table)
|
||||
else:
|
||||
return s.translate(None, bad_chars)
|
||||
|
||||
|
||||
def asciidammit(s):
|
||||
if type(s) is str:
|
||||
return asciionly(s)
|
||||
elif type(s) is unicode:
|
||||
return asciionly(s.encode('ascii', 'ignore'))
|
||||
else:
|
||||
return asciidammit(unicode(s))
|
||||
|
||||
|
||||
def make_type_consistent(s1, s2):
|
||||
if isinstance(s1, str) and isinstance(s2, str):
|
||||
return s1, s2
|
||||
|
||||
elif isinstance(s1, unicode) and isinstance(s2, unicode):
|
||||
return s1, s2
|
||||
|
||||
else:
|
||||
return unicode(s1), unicode(s2)
|
||||
|
||||
|
||||
def full_process(s, force_ascii=False):
|
||||
"""Process string by
|
||||
-- removing all but letters and numbers
|
||||
-- trim whitespace
|
||||
-- force to lower case
|
||||
if force_ascii == True, force convert to ascii"""
|
||||
|
||||
if s is None:
|
||||
return ""
|
||||
|
||||
if force_ascii:
|
||||
s = asciidammit(s)
|
||||
# Keep only Letters and Numbres (see Unicode docs).
|
||||
string_out = StringProcessor.replace_non_letters_non_numbers_with_whitespace(s)
|
||||
# Force into lowercase.
|
||||
string_out = StringProcessor.to_lower_case(string_out)
|
||||
# Remove leading and trailing whitespaces.
|
||||
string_out = StringProcessor.strip(string_out)
|
||||
return string_out
|
||||
|
||||
|
||||
def intr(n):
|
||||
'''Returns a correctly rounded integer'''
|
||||
return int(round(n))
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import htmlentitydefs
|
||||
|
||||
RE = {
|
||||
'space': re.compile('[ ]{2,}', re.U | re.S),
|
||||
'cl': re.compile('[\n]{2,}', re.U | re.S),
|
||||
'br': re.compile('<\s*br[\s/]*>', re.U | re.S),
|
||||
'inner': re.compile('<[^>]*>[^<]+<\s*/[^>]*>', re.U | re.S),
|
||||
'html': re.compile('<[^>]*>', re.U | re.S),
|
||||
'entity': re.compile('&#?\w+;', re.U)
|
||||
}
|
||||
|
||||
UNSUPPORT = {
|
||||
'—': '-'
|
||||
}
|
||||
|
||||
|
||||
class Clear:
|
||||
def text(self, text, inner=False):
|
||||
text = self._unsupport(text).replace(u'\r', u'\n')
|
||||
text = RE['br'].sub(u'\n', text)
|
||||
if inner:
|
||||
text = RE['inner'].sub(u'', text)
|
||||
text = RE['html'].sub(u'', text)
|
||||
text = self.char(text)
|
||||
text = RE['space'].sub(u' ', text)
|
||||
return RE['cl'].sub(u'\n', text).strip()
|
||||
|
||||
def string(self, text, space=u''):
|
||||
return self.text(text).replace(u'\n', space).strip()
|
||||
|
||||
def char(self, text):
|
||||
return RE['entity'].sub(self._unescape, self._unsupport(text))
|
||||
|
||||
def _unsupport(self, text):
|
||||
for tag, value in UNSUPPORT.iteritems():
|
||||
text = text.replace(tag, value)
|
||||
return text
|
||||
|
||||
def _unescape(self, m):
|
||||
text = m.group(0)
|
||||
if text[:2] == u"&#":
|
||||
try:
|
||||
if text[:3] == u"&#x":
|
||||
return unichr(int(text[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text
|
||||
@@ -0,0 +1,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Russian metadata plugin for Plex, which uses http://www.kinopoisk.ru/ to get the tag data.
|
||||
# Плагин для обновления информации о фильмах использующий КиноПоиск (http://www.kinopoisk.ru/).
|
||||
# Copyright (C) 2012 Yevgeny Nyden
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# @author zhenya (Yevgeny Nyden)
|
||||
# @revision 148
|
||||
|
||||
import urllib
|
||||
import urllib2
|
||||
import base64
|
||||
import time
|
||||
|
||||
import LOGGER as Log
|
||||
|
||||
|
||||
class HTTP:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def fetch(self, request, **kwargs):
|
||||
self.con, self.fd, self.progress, self.cookies, self.request = None, None, None, None, request
|
||||
|
||||
if not isinstance(self.request, HTTPRequest):
|
||||
self.request = HTTPRequest(url=self.request, **kwargs)
|
||||
|
||||
self.response = HTTPResponse(self.request)
|
||||
|
||||
Log.Debug('XBMCup: HTTP: request: ' + str(self.request))
|
||||
try:
|
||||
self._opener()
|
||||
self._fetch()
|
||||
except Exception, e:
|
||||
Log.Debug('XBMCup: HTTP: ' + str(e))
|
||||
if isinstance(e, urllib2.HTTPError):
|
||||
self.response.code = e.code
|
||||
self.response.error = e
|
||||
else:
|
||||
self.response.code = 200
|
||||
|
||||
if self.fd:
|
||||
self.fd.close()
|
||||
self.fd = None
|
||||
|
||||
if self.con:
|
||||
self.con.close()
|
||||
self.con = None
|
||||
|
||||
if self.progress:
|
||||
self.progress.close()
|
||||
self.progress = None
|
||||
|
||||
self.response.time = time.time() - self.response.time
|
||||
|
||||
return self.response
|
||||
|
||||
|
||||
def _opener(self):
|
||||
|
||||
build = [urllib2.HTTPHandler()]
|
||||
|
||||
if self.request.redirect:
|
||||
build.append(urllib2.HTTPRedirectHandler())
|
||||
|
||||
if self.request.proxy_host and self.request.proxy_port:
|
||||
build.append(urllib2.ProxyHandler(
|
||||
{self.request.proxy_protocol: self.request.proxy_host + ':' + str(self.request.proxy_port)}))
|
||||
|
||||
if self.request.proxy_username:
|
||||
proxy_auth_handler = urllib2.ProxyBasicAuthHandler()
|
||||
proxy_auth_handler.add_password('realm', 'uri', self.request.proxy_username,
|
||||
self.request.proxy_password)
|
||||
build.append(proxy_auth_handler)
|
||||
|
||||
urllib2.install_opener(urllib2.build_opener(*build))
|
||||
|
||||
|
||||
def _fetch(self):
|
||||
params = {} if self.request.params is None else self.request.params
|
||||
|
||||
if self.request.method == 'POST':
|
||||
if isinstance(params, dict) or isinstance(params, list):
|
||||
params = urllib.urlencode(params)
|
||||
req = urllib2.Request(self.request.url, params)
|
||||
else:
|
||||
req = urllib2.Request(self.request.url)
|
||||
|
||||
for key, value in self.request.headers.iteritems():
|
||||
req.add_header(key, value)
|
||||
|
||||
if self.request.auth_username and self.request.auth_password:
|
||||
req.add_header('Authorization', 'Basic %s' % base64.encodestring(
|
||||
':'.join([self.request.auth_username, self.request.auth_password])).strip())
|
||||
|
||||
# self.con = urllib2.urlopen(req, timeout=self.request.timeout)
|
||||
self.con = urllib2.urlopen(req)
|
||||
self.response.headers = self._headers(self.con.info())
|
||||
|
||||
self.response.body = self.con.read()
|
||||
|
||||
if self.request.cookies:
|
||||
self.cookies.save(self.request.cookies)
|
||||
|
||||
|
||||
def _headers(self, raw):
|
||||
headers = {}
|
||||
for line in raw.headers:
|
||||
pair = line.split(':', 1)
|
||||
if len(pair) == 2:
|
||||
tag = pair[0].lower().strip()
|
||||
value = pair[1].strip()
|
||||
if tag and value:
|
||||
headers[tag] = value
|
||||
return headers
|
||||
|
||||
|
||||
def _progress(self, read, size, name):
|
||||
res = []
|
||||
if size < 0:
|
||||
res.append(1)
|
||||
else:
|
||||
res.append(int(float(read) / (float(size) / 100.0)))
|
||||
if name:
|
||||
res.append(u'File: ' + name)
|
||||
if size != -1:
|
||||
res.append(u'Size: ' + self._human(size))
|
||||
res.append(u'Load: ' + self._human(read))
|
||||
return res
|
||||
|
||||
def _human(self, size):
|
||||
human = None
|
||||
for h, f in (('KB', 1024), ('MB', 1024 * 1024), ('GB', 1024 * 1024 * 1024), ('TB', 1024 * 1024 * 1024 * 1024)):
|
||||
if size / f > 0:
|
||||
human = h
|
||||
factor = f
|
||||
else:
|
||||
break
|
||||
if human is None:
|
||||
return (u'%10.1f %s' % (size, u'byte')).replace(u'.0', u'')
|
||||
else:
|
||||
return u'%10.2f %s' % (float(size) / float(factor), human)
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
def __init__(self, url, method='GET', headers=None, cookies=None, params=None, upload=None, download=None,
|
||||
progress=False, auth_username=None, auth_password=None, proxy_protocol='http', proxy_host=None,
|
||||
proxy_port=None, proxy_username=None, proxy_password='', timeout=20.0, redirect=True, gzip=False):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.headers = headers
|
||||
|
||||
self.cookies = cookies
|
||||
|
||||
self.params = params
|
||||
|
||||
self.upload = upload
|
||||
self.download = download
|
||||
self.progress = progress
|
||||
|
||||
self.auth_username = auth_username
|
||||
self.auth_password = auth_password
|
||||
|
||||
self.proxy_protocol = proxy_protocol
|
||||
self.proxy_host = proxy_host
|
||||
self.proxy_port = proxy_port
|
||||
self.proxy_username = proxy_username
|
||||
self.proxy_password = proxy_password
|
||||
|
||||
self.timeout = timeout
|
||||
|
||||
self.redirect = redirect
|
||||
|
||||
self.gzip = gzip
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (self.__class__.__name__, ','.join('%s=%r' % i for i in self.__dict__.iteritems()))
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.code = None
|
||||
self.headers = {}
|
||||
self.error = None
|
||||
self.body = None
|
||||
self.filename = None
|
||||
self.time = time.time()
|
||||
|
||||
def __repr__(self):
|
||||
args = ','.join('%s=%r' % i for i in self.__dict__.iteritems() if i[0] != 'body')
|
||||
if self.body:
|
||||
args += ',body=<data>'
|
||||
else:
|
||||
args += ',body=None'
|
||||
return '%s(%s)' % (self.__class__.__name__, args)
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
try:
|
||||
import xbmcaddon
|
||||
|
||||
__settings__ = xbmcaddon.Addon("plugin.video.torrenter")
|
||||
debug = __settings__.getSetting("debug")
|
||||
except:
|
||||
debug = 'true'
|
||||
|
||||
|
||||
def Log(msg, force=False):
|
||||
try:
|
||||
print "[torrenter log] " + msg
|
||||
except UnicodeEncodeError:
|
||||
print "[torrenter log] " + msg.encode("utf-8", "ignore")
|
||||
|
||||
|
||||
def Debug(msg, force=False):
|
||||
if debug == 'true' or force:
|
||||
try:
|
||||
print "[torrenter] " + msg
|
||||
except UnicodeEncodeError:
|
||||
print "[torrenter] " + msg.encode("utf-8", "ignore")
|
||||
|
||||
|
||||
def Info(msg, force=False):
|
||||
if debug == 'true' or force:
|
||||
try:
|
||||
print "[torrenter] " + msg
|
||||
except UnicodeEncodeError:
|
||||
print "[torrenter] " + msg.encode("utf-8", "ignore")
|
||||
|
||||
|
||||
def Warn(msg, force=False):
|
||||
if debug == 'true' or force:
|
||||
try:
|
||||
print "[torrenter] " + msg
|
||||
except UnicodeEncodeError:
|
||||
print "[torrenter] " + msg.encode("utf-8", "ignore")
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Russian metadata plugin for Plex, which uses http://www.kinopoisk.ru/ to get the tag data.
|
||||
# Плагин для обновления информации о фильмах использующий КиноПоиск (http://www.kinopoisk.ru/).
|
||||
# Copyright (C) 2012 Yevgeny Nyden
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# @author zhenya (Yevgeny Nyden)
|
||||
# @version 1.52
|
||||
# @revision 148
|
||||
|
||||
import common
|
||||
import pageparser
|
||||
import LOGGER
|
||||
import pluginsettings as S
|
||||
from translit import provide_unicode
|
||||
|
||||
|
||||
IS_DEBUG = True # TODO - DON'T FORGET TO SET IT TO FALSE FOR A DISTRO.
|
||||
|
||||
# Plugin preferences.
|
||||
# When changing default values here, also update the DefaultPrefs.json file.
|
||||
PREFS = common.Preferences(
|
||||
('kinopoisk_pref_max_posters', S.KINOPOISK_PREF_DEFAULT_MAX_POSTERS),
|
||||
('kinopoisk_pref_max_art', S.KINOPOISK_PREF_DEFAULT_MAX_ART),
|
||||
('kinopoisk_pref_get_all_actors', S.KINOPOISK_PREF_DEFAULT_GET_ALL_ACTORS),
|
||||
('kinopoisk_pref_imdb_support', S.KINOPOISK_PREF_DEFAULT_IMDB_SUPPORT),
|
||||
(None, None),
|
||||
('kinopoisk_pref_imdb_rating', S.KINOPOISK_PREF_DEFAULT_IMDB_RATING),
|
||||
('kinopoisk_pref_kp_rating', S.KINOPOISK_PREF_DEFAULT_KP_RATING))
|
||||
|
||||
|
||||
def Start():
|
||||
LOGGER.Info('***** START ***** %s' % common.USER_AGENT)
|
||||
PREFS.readPluginPreferences()
|
||||
|
||||
|
||||
def ValidatePrefs():
|
||||
LOGGER.Info('***** updating preferences...')
|
||||
PREFS.readPluginPreferences()
|
||||
|
||||
|
||||
class KinoPoiskRuAgent():
|
||||
name = 'KinoPoiskRu'
|
||||
primary_provider = True
|
||||
fallback_agent = False
|
||||
accepts_from = ['com.plexapp.agents.localmedia']
|
||||
contributes_to = None
|
||||
parser = pageparser.PageParser(
|
||||
LOGGER, common.HttpUtils(S.ENCODING_KINOPOISK_PAGE, pageparser.USER_AGENT), IS_DEBUG)
|
||||
|
||||
|
||||
# #############################################################################
|
||||
############################# S E A R C H ####################################
|
||||
##############################################################################
|
||||
def search(self, results, media, lang, manual=False):
|
||||
""" Searches for matches on KinoPoisk using the title and year
|
||||
passed via the media object. All matches are saved in a list of results
|
||||
as MetadataSearchResult objects. For each results, we determine a
|
||||
page id, title, year, and the score (how good we think the match
|
||||
is on the scale of 1 - 100).
|
||||
"""
|
||||
LOGGER.Debug('SEARCH START <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')
|
||||
try:
|
||||
mediaName = media['name'].decode('utf-8')
|
||||
except:
|
||||
mediaName = media['name']
|
||||
mediaYear = media['year']
|
||||
LOGGER.Debug('searching for name="%s", year="%s"...' %
|
||||
(mediaName, str(mediaYear)))
|
||||
|
||||
# Look for matches on KinoPisk (result is returned as an array of tuples [kinoPoiskId, title, year, score]).
|
||||
titleResults = KinoPoiskRuAgent.parser.fetchAndParseSearchResults(mediaName, mediaYear)
|
||||
for titleResult in titleResults:
|
||||
results.append((titleResult[0], titleResult[1], titleResult[2], lang, titleResult[3]))
|
||||
|
||||
# Sort results according to their score (Сортируем результаты).
|
||||
LOGGER.Debug('SEARCH END <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')
|
||||
return results
|
||||
|
||||
#k=KinoPoiskRuAgent()
|
||||
|
||||
#print str(k.search([],{'name':'Django Unchained','year':'2012'},'English'))
|
||||
@@ -0,0 +1,382 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Russian metadata plugin for Plex, which uses http://www.kinopoisk.ru/ to get the tag data.
|
||||
# Плагин для обновления информации о фильмах использующий КиноПоиск (http://www.kinopoisk.ru/).
|
||||
# Copyright (C) 2012 Yevgeny Nyden
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# @author zhenya (Yevgeny Nyden)
|
||||
# @revision 148
|
||||
|
||||
import sys
|
||||
import math
|
||||
import difflib
|
||||
|
||||
import translit
|
||||
import LOGGER as Log
|
||||
from HTTP import *
|
||||
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22'
|
||||
# USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/534.51.22'
|
||||
ENCODING_PLEX = 'utf-8'
|
||||
|
||||
SCORE_PENALTY_ITEM_ORDER = 2
|
||||
SCORE_PENALTY_YEAR = 30
|
||||
SCORE_PENALTY_TITLE = 40
|
||||
|
||||
IMAGE_SCORE_MAX_NUMBER_OF_ITEMS = 5
|
||||
IMAGE_SCORE_ITEM_ORDER_BONUS_MAX = 25
|
||||
IMAGE_SCORE_RESOLUTION_BONUS_MAX = 25
|
||||
IMAGE_SCORE_RATIO_BONUS_MAX = 45
|
||||
IMAGE_SCORE_THUMB_BONUS = 5
|
||||
POSTER_SCORE_MIN_RESOLUTION_PX = 60 * 1000
|
||||
POSTER_SCORE_MAX_RESOLUTION_PX = 600 * 1000
|
||||
POSTER_SCORE_BEST_RATIO = 0.7
|
||||
ART_SCORE_BEST_RATIO = 1.5
|
||||
ART_SCORE_MIN_RESOLUTION_PX = 200 * 1000
|
||||
ART_SCORE_MAX_RESOLUTION_PX = 1000 * 1000
|
||||
|
||||
|
||||
class Preferences:
|
||||
""" These instance variables are populated from plugin preferences.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
(maxPostersName, maxPostersDefault),
|
||||
(maxArtName, maxArtDefault),
|
||||
(getAllActorsName, getAllActorsDefault),
|
||||
(imdbSupportName, imdbSupportDefault),
|
||||
(cacheTimeName, cacheTimeDefault),
|
||||
(imdbRatingName, imdbRatingDefault),
|
||||
(additionalRatingName, additionalRatingDefault)):
|
||||
self.maxPostersName = maxPostersName
|
||||
self.maxPosters = maxPostersDefault
|
||||
self.maxArtName = maxArtName
|
||||
self.maxArt = maxArtDefault
|
||||
self.getAllActorsName = getAllActorsName
|
||||
self.getAllActors = getAllActorsDefault
|
||||
self.imdbSupportName = imdbSupportName
|
||||
self.imdbSupport = imdbSupportDefault
|
||||
self.cacheTimeName = cacheTimeName
|
||||
self.cacheTime = cacheTimeDefault
|
||||
self.cacheTimeDefault = cacheTimeDefault
|
||||
self.imdbRatingName = imdbRatingName
|
||||
self.imdbRating = imdbRatingDefault
|
||||
self.additionalRatingName = additionalRatingName
|
||||
self.additionalRating = additionalRatingDefault
|
||||
|
||||
def readPluginPreferences(self):
|
||||
# Setting image (poster and funart) preferences.
|
||||
if self.maxPostersName is not None:
|
||||
self.maxPosters = int(Prefs[self.maxPostersName])
|
||||
Log.Debug('PREF: Max poster results is set to %d.' % self.maxPosters)
|
||||
if self.maxArtName is not None:
|
||||
self.maxArt = int(Prefs[self.maxArtName])
|
||||
Log.Debug('PREF: Max art results is set to %d.' % self.maxArt)
|
||||
if self.getAllActorsName is not None:
|
||||
self.getAllActors = Prefs[self.getAllActorsName]
|
||||
Log.Debug('PREF: Parse all actors is set to %s.' % str(self.getAllActors))
|
||||
|
||||
# Setting IMDB support.
|
||||
if self.imdbSupportName is not None:
|
||||
self.imdbSupport = Prefs[self.imdbSupportName]
|
||||
Log.Debug('PREF: IMDB support is set to %s.' % str(self.imdbSupport))
|
||||
|
||||
# Setting cache expiration time.
|
||||
if self.cacheTimeName is not None:
|
||||
self.cacheTime = parseAndSetCacheTimeFromPrefs(self.cacheTimeName, self.cacheTimeDefault)
|
||||
|
||||
# Setting IMDB rating.
|
||||
if self.imdbRatingName is not None:
|
||||
self.imdbRating = Prefs[self.imdbRatingName]
|
||||
Log.Debug('PREF: IMDB rating is set to %s.' % str(self.imdbRating))
|
||||
|
||||
# Setting kinopoisk.ru rating.
|
||||
if self.additionalRatingName is not None:
|
||||
self.additionalRating = Prefs[self.additionalRatingName]
|
||||
Log.Debug('PREF: kinopoisk rating is set to %s.' % str(self.additionalRating))
|
||||
|
||||
|
||||
def getElementFromHttpRequest(url, encoding, userAgent=USER_AGENT):
|
||||
""" Fetches a given URL and returns it as an element.
|
||||
Функция преобразования html-кода в xml-код.
|
||||
"""
|
||||
for i in range(3):
|
||||
errorCount = 0
|
||||
try:
|
||||
req = HTTPRequest(url, headers={'User-agent': userAgent, 'Accept': 'text/html'})
|
||||
response = HTTP().fetch(req)
|
||||
return str(response.body).decode(encoding)
|
||||
except:
|
||||
errorCount += 1
|
||||
Log.Debug('Error fetching URL: "%s".' % url)
|
||||
time.sleep(1 + errorCount)
|
||||
return None
|
||||
|
||||
|
||||
def requestImageJpeg(url, userAgent):
|
||||
""" Requests an image given its URL and returns a request object.
|
||||
"""
|
||||
try:
|
||||
response = HTTP.Request(url, headers={
|
||||
'User-agent': userAgent,
|
||||
'Accept': 'image/jpeg'
|
||||
})
|
||||
return response
|
||||
except:
|
||||
Log.Debug('Error fetching URL: "%s".' % url)
|
||||
return None
|
||||
|
||||
|
||||
def getWin1252ResponseFromHttpRequest(url):
|
||||
""" Requests an image given its URL and returns a request object.
|
||||
"""
|
||||
try:
|
||||
response = HTTP.Request(url, headers={
|
||||
'User-agent': USER_AGENT,
|
||||
'Accept-Charset': 'ISO-8859-1;q=0.7,*;q=0.3',
|
||||
'Accept-Language': 'en-US,en;q=0.8'
|
||||
})
|
||||
return response
|
||||
except:
|
||||
Log.Error('Error fetching URL: "%s".' % url)
|
||||
return None
|
||||
|
||||
|
||||
def printSearchArrayResults(results):
|
||||
""" Sends a list of media array results to debug log.
|
||||
"""
|
||||
Log.Debug('Search produced %d results:' % len(results))
|
||||
index = -1
|
||||
for result in results:
|
||||
index += 1
|
||||
Log.Debug(' ... %d: id="%s", name="%s", year="%s", score="%d".' %
|
||||
(index, result[0], result[1], str(result[2]), result[3]))
|
||||
|
||||
|
||||
def printSearchResults(results):
|
||||
""" Sends a list of media results to debug log.
|
||||
"""
|
||||
Log.Debug('Search produced %d results:' % len(results))
|
||||
index = 0
|
||||
for result in results:
|
||||
Log.Debug(' ... %d: id="%s", name="%s", year="%s", score="%d".' %
|
||||
(index, result.id, result.name, str(result.year), result.score))
|
||||
index += 1
|
||||
|
||||
|
||||
def printImageSearchResults(thumbnailList):
|
||||
Log.Debug('printing %d image results:' % len(thumbnailList))
|
||||
index = 0
|
||||
for result in thumbnailList:
|
||||
Log.Debug(' ... %d: index=%s, score=%s, URL="%s".' %
|
||||
(index, result.index, result.score, result.url))
|
||||
index += 1
|
||||
return None
|
||||
|
||||
|
||||
def logException(msg):
|
||||
excInfo = sys.exc_info()
|
||||
Log.Exception('%s; exception: %s; cause: %s' % (msg, excInfo[0], excInfo[1]))
|
||||
|
||||
|
||||
def scoreMediaTitleMatch(mediaName, mediaYear, title, altTitle, year, itemIndex):
|
||||
""" Compares page and media titles taking into consideration
|
||||
media item's year and title values. Returns score [0, 100].
|
||||
Search item scores 100 when:
|
||||
- it's first on the list of results; AND
|
||||
- it equals to the media title (ignoring case) OR all media title words are found in the search item; AND
|
||||
- search item year equals to media year.
|
||||
|
||||
For now, our title scoring is pretty simple - we check if individual words
|
||||
from media item's title are found in the title from search results.
|
||||
We should also take into consideration order of words, so that "One Two" would not
|
||||
have the same score as "Two One". Also, taking into consideration year difference.
|
||||
"""
|
||||
# TODO(zhenya): add logging that works in unit tests.
|
||||
# Log.Debug('comparing item %d::: "%s-%s" with "%s-%s" (%s)...' %
|
||||
# (itemIndex, str(mediaName), str(mediaYear), str(title), str(year), str(altTitle)))
|
||||
# Max score is when both title and year match exactly.
|
||||
score = 100
|
||||
|
||||
# Item order penalty (the lower it is on the list or results, the larger the penalty).
|
||||
score = score - (itemIndex * SCORE_PENALTY_ITEM_ORDER)
|
||||
|
||||
# Compute year penalty: [equal, diff>=3] --> [0, MAX].
|
||||
yearPenalty = SCORE_PENALTY_YEAR
|
||||
mediaYear = toInteger(mediaYear)
|
||||
year = toInteger(year)
|
||||
if mediaYear is not None and year is not None:
|
||||
yearDiff = abs(mediaYear - year)
|
||||
if not yearDiff:
|
||||
yearPenalty = 0
|
||||
elif yearDiff == 1:
|
||||
yearPenalty = 15
|
||||
elif yearDiff == 2:
|
||||
yearPenalty = 25
|
||||
score = score - yearPenalty
|
||||
#print str(mediaYear)+' '+str(year)+' '+str(yearPenalty)
|
||||
|
||||
# Compute title penalty.
|
||||
titlePenalty = computeTitlePenalty(mediaName, title)
|
||||
altTitlePenalty = 100
|
||||
if altTitle is not None:
|
||||
altTitlePenalty = computeTitlePenalty(mediaName, altTitle)
|
||||
|
||||
# Get detranlitirated media name (in case filename is in latin characters),
|
||||
# compare it's score with the original, and pick the min.
|
||||
try:
|
||||
altMediaName = translit.detranslify(mediaName)
|
||||
detranslifiedTitlePenalty = computeTitlePenalty(altMediaName, title)
|
||||
titlePenalty = min(detranslifiedTitlePenalty, titlePenalty)
|
||||
if altTitle is not None:
|
||||
detranslifiedAltTitlePenalty = computeTitlePenalty(altMediaName, altTitle)
|
||||
altTitlePenalty = min(detranslifiedAltTitlePenalty, altTitlePenalty)
|
||||
except:
|
||||
pass
|
||||
|
||||
titlePenalty = min(titlePenalty, altTitlePenalty)
|
||||
score = score - titlePenalty
|
||||
|
||||
# If the score is not high enough, add a few points to the first result -
|
||||
# let's give KinoPoisk some credit :-).
|
||||
if itemIndex == 0 and score <= 80:
|
||||
score = score + 5
|
||||
|
||||
# IMPORTANT: always return an int.
|
||||
score = int(score)
|
||||
# Log.Debug('***** title scored %d' % score)
|
||||
return score
|
||||
|
||||
|
||||
def scoreThumbnailResult(thumb, isPoster):
|
||||
""" Given a Thumbnail object that represents an poster or a funart result,
|
||||
scores it, and stores the score on the passed object (thumb.score).
|
||||
"""
|
||||
score = 0
|
||||
if thumb.url is None:
|
||||
thumb.score = 0
|
||||
return
|
||||
|
||||
if thumb.index < IMAGE_SCORE_MAX_NUMBER_OF_ITEMS:
|
||||
# Score bonus from index for items below 10 on the list.
|
||||
bonus = IMAGE_SCORE_ITEM_ORDER_BONUS_MAX * \
|
||||
((IMAGE_SCORE_MAX_NUMBER_OF_ITEMS - thumb.index) / float(IMAGE_SCORE_MAX_NUMBER_OF_ITEMS))
|
||||
score += bonus
|
||||
|
||||
if thumb.width is not None and thumb.height is not None:
|
||||
# Get a resolution bonus if width*height is more than a certain min value.
|
||||
if isPoster:
|
||||
minPx = POSTER_SCORE_MIN_RESOLUTION_PX
|
||||
maxPx = POSTER_SCORE_MAX_RESOLUTION_PX
|
||||
bestRatio = POSTER_SCORE_BEST_RATIO
|
||||
else:
|
||||
minPx = ART_SCORE_MIN_RESOLUTION_PX
|
||||
maxPx = ART_SCORE_MAX_RESOLUTION_PX
|
||||
bestRatio = ART_SCORE_BEST_RATIO
|
||||
pixelsCount = thumb.width * thumb.height
|
||||
if pixelsCount > minPx:
|
||||
if pixelsCount > maxPx:
|
||||
pixelsCount = maxPx
|
||||
bonus = float(IMAGE_SCORE_RESOLUTION_BONUS_MAX) * \
|
||||
float((pixelsCount - minPx)) / float((maxPx - minPx))
|
||||
score += bonus
|
||||
|
||||
# Get an orientation (Portrait vs Landscape) bonus. (we prefer images that are have portrait orientation.
|
||||
ratio = thumb.width / float(thumb.height)
|
||||
ratioDiff = math.fabs(bestRatio - ratio)
|
||||
if ratioDiff < 0.5:
|
||||
bonus = IMAGE_SCORE_RATIO_BONUS_MAX * (0.5 - ratioDiff) * 2.0
|
||||
score += bonus
|
||||
|
||||
# Get a bonus if image has a separate thumbnail URL.
|
||||
if thumb.thumbUrl is not None and thumb.url != thumb.thumbUrl:
|
||||
score += IMAGE_SCORE_THUMB_BONUS
|
||||
|
||||
thumb.score = int(score)
|
||||
|
||||
|
||||
def isAsciiString(mediaName):
|
||||
""" Returns True if all characters of the string are ASCII.
|
||||
"""
|
||||
for index, char in enumerate(mediaName):
|
||||
if ord(char) >= 128:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def toInteger(maybeNumber):
|
||||
""" Returns the argument converted to an integer if it represents a number
|
||||
or None if the argument is None or does not represent a number.
|
||||
"""
|
||||
try:
|
||||
if maybeNumber is not None and str(maybeNumber).strip() != '':
|
||||
return int(maybeNumber)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def computeTitlePenalty(mediaName, title):
|
||||
""" Given media name and a candidate title, returns the title result score penalty.
|
||||
@param mediaName Movie title parsed from the file system.
|
||||
@param title Movie title from the website.
|
||||
"""
|
||||
mediaName = mediaName.lower()
|
||||
title = title.lower()
|
||||
if mediaName != title:
|
||||
# First approximate the whole strings.
|
||||
diffRatio = difflib.SequenceMatcher(None, mediaName, title).ratio()
|
||||
penalty = int(SCORE_PENALTY_TITLE * (1 - diffRatio))
|
||||
|
||||
# If the penalty is more than 1/2 of max title penalty, check to see if
|
||||
# this title starts with media name. This means that media name does not
|
||||
# have the whole name of the movie - very common case. For example, media name
|
||||
# "Кавказская пленница" for a movie title "Кавказская пленница, или Новые приключения Шурика".
|
||||
if penalty >= 15: # This is so that we don't have to do the 'split' every time.
|
||||
# Compute the scores of the
|
||||
# First, check if the title starts with media name.
|
||||
mediaNameParts = mediaName.split()
|
||||
titleParts = title.split()
|
||||
if len(mediaNameParts) <= len(titleParts):
|
||||
i = 0
|
||||
# Start with some small penalty, value of which depends on how
|
||||
# many words media name has relative to the title's word count.
|
||||
penaltyAlt = max(5, int(round((1.0 - (float(len(mediaNameParts)) / len(titleParts))) * 15 - 5)))
|
||||
penaltyPerPart = SCORE_PENALTY_TITLE / len(mediaNameParts)
|
||||
for mediaNamePart in mediaNameParts:
|
||||
partDiffRatio = difflib.SequenceMatcher(None, mediaNamePart, titleParts[i]).ratio()
|
||||
penaltyAlt = penaltyAlt + int(penaltyPerPart * (1 - partDiffRatio))
|
||||
i = i + 1
|
||||
penalty = min(penalty, penaltyAlt)
|
||||
# print '++++++ DIFF("%s", "%s") = %g --> %d' % (mediaName.encode('utf8'), title.encode('utf8'), diffRatio, penalty)
|
||||
# Log.Debug('++++++ DIFF("%s", "%s") = %g --> %d' % (mediaName.encode('utf8'), title.encode('utf8'), diffRatio, penalty))
|
||||
return penalty
|
||||
return 0
|
||||
|
||||
|
||||
class HttpUtils():
|
||||
def __init__(self, encoding, userAgent):
|
||||
self.encoding = encoding
|
||||
self.userAgent = userAgent
|
||||
|
||||
def requestAndParseHtmlPage(self, url):
|
||||
return getElementFromHttpRequest(url, self.encoding, self.userAgent)
|
||||
|
||||
def requestImageJpeg(self, url):
|
||||
return requestImageJpeg(url, self.userAgent)
|
||||
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Russian metadata plugin for Plex, which uses http://www.kinopoisk.ru/ to get the tag data.
|
||||
# Плагин для обновления информации о фильмах использующий КиноПоиск (http://www.kinopoisk.ru/).
|
||||
# Copyright (C) 2013 Yevgeny Nyden
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# @author zhenya (Yevgeny Nyden)
|
||||
# @version 1.52
|
||||
# @revision 148
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import operator
|
||||
|
||||
import common
|
||||
import pluginsettings as S
|
||||
import translit
|
||||
|
||||
|
||||
# MATCHER_MOVIE_DURATION = re.compile('\s*(\d+).*?', re.UNICODE | re.DOTALL)
|
||||
#MATCHER_IMDB_RATING = re.compile('IMDb:\s*(\d+\.?\d*)\s*\(\s*([\s\d]+)\s*\)', re.UNICODE | re.DOTALL)
|
||||
#MATCHER_IMDB_RATING = re.compile('IMDb:\s*(\d+\.?\d*)\s?\((.*)\)', re.UNICODE)
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22'
|
||||
'''
|
||||
MOVIE_THUMBNAIL_SMALL_WIDTH = 130
|
||||
MOVIE_THUMBNAIL_SMALL_HEIGHT = 168
|
||||
MOVIE_THUMBNAIL_BIG_WIDTH = 780
|
||||
MOVIE_THUMBNAIL_BIG_HEIGHT = 1024
|
||||
|
||||
# Compiled regex matchers.
|
||||
MATCHER_WIDTH_FROM_STYLE = re.compile('.*width\s*:\s*(\d+)px.*', re.UNICODE)
|
||||
MATCHER_HEIGHT_FROM_STYLE = re.compile('.*height\s*:\s*(\d+)px.*', re.UNICODE)
|
||||
MATCHER_LEADING_NONALPHA = re.compile('^[\s\d\.\(\)]*', re.UNICODE | re.MULTILINE)
|
||||
|
||||
|
||||
# Русские месяца, пригодятся для определения дат.
|
||||
RU_MONTH = {
|
||||
u'января': '01',
|
||||
u'февраля': '02',
|
||||
u'марта': '03',
|
||||
u'апреля': '04',
|
||||
u'мая': '05',
|
||||
u'июня': '06',
|
||||
u'июля': '07',
|
||||
u'августа': '08',
|
||||
u'сентября': '09',
|
||||
u'октября': '10',
|
||||
u'ноября': '11',
|
||||
u'декабря': '12'
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class PageParser:
|
||||
def __init__(self, logger, httpUtils=common.HttpUtils(S.ENCODING_KINOPOISK_PAGE, USER_AGENT), isDebug=False):
|
||||
self.log = logger
|
||||
self.isDebug = isDebug
|
||||
self.httpUtils = httpUtils
|
||||
|
||||
def fetchAndParseSearchResults(self, mediaName, mediaYear, mediaAltName=None):
|
||||
""" Searches for movie titles on KinoPoisk.
|
||||
@param mediaName Movie title parsed from a filename.
|
||||
@param mediaName Movie year parsed from a filename.
|
||||
@return Array of tuples: [kinoPoiskId, title, year, score]
|
||||
"""
|
||||
self.log.Info('Quering kinopoisk...')
|
||||
results = self.queryKinoPoisk(mediaName, mediaYear)
|
||||
|
||||
# Check media name is all ASCII characters, and if it is,
|
||||
# issue another query to KinoPoisk using a translified media name;
|
||||
# lastly, merge the scored results.
|
||||
if common.isAsciiString(mediaName):
|
||||
translifiedMediaName = translit.detranslify(mediaName)
|
||||
moreResults = self.queryKinoPoisk(translifiedMediaName, mediaYear)
|
||||
resultsMap = dict()
|
||||
for result in results:
|
||||
resultsMap[result[0]] = result
|
||||
results = [] # Recreate and repopulate the results array removing duplicates.
|
||||
for result in moreResults:
|
||||
currId = result[0]
|
||||
if currId in resultsMap.keys():
|
||||
origResult = resultsMap[currId]
|
||||
del resultsMap[currId]
|
||||
if result[3] >= origResult[3]:
|
||||
results.append(result)
|
||||
else:
|
||||
results.append(origResult)
|
||||
else:
|
||||
results.append(result)
|
||||
results.extend(resultsMap.viewvalues())
|
||||
|
||||
if mediaAltName:
|
||||
moreResults = self.queryKinoPoisk(mediaAltName, mediaYear)
|
||||
resultsMap = dict()
|
||||
for result in results:
|
||||
resultsMap[result[0]] = result
|
||||
results = [] # Recreate and repopulate the results array removing duplicates.
|
||||
for result in moreResults:
|
||||
currId = result[0]
|
||||
if currId in resultsMap.keys():
|
||||
origResult = resultsMap[currId]
|
||||
del resultsMap[currId]
|
||||
if result[3] >= origResult[3]:
|
||||
results.append(result)
|
||||
else:
|
||||
results.append(origResult)
|
||||
else:
|
||||
results.append(result)
|
||||
results.extend(resultsMap.viewvalues())
|
||||
|
||||
# Sort all results based on their score.
|
||||
results.sort(key=operator.itemgetter(3))
|
||||
results.reverse()
|
||||
if self.isDebug:
|
||||
self.log.Debug('Search produced %d results:' % len(results))
|
||||
index = -1
|
||||
for result in results:
|
||||
index += 1
|
||||
self.log.Debug(' ... %d: id="%s", name="%s", year="%s", score="%d".' %
|
||||
(index, result[0], result[1], str(result[2]), result[3]))
|
||||
return results
|
||||
|
||||
def queryKinoPoisk(self, mediaName, mediaYear):
|
||||
""" Ищет фильм на кинопоиске.
|
||||
Returns title results as they are returned (no sorting is done here!).
|
||||
"""
|
||||
results = []
|
||||
try:
|
||||
encodedName = urllib.quote(mediaName.encode(S.ENCODING_KINOPOISK_PAGE))
|
||||
except:
|
||||
encodedName = urllib.quote(mediaName.encode('utf-8'))
|
||||
page = self.httpUtils.requestAndParseHtmlPage(S.KINOPOISK_SEARCH_SIMPLE % encodedName)
|
||||
if page is None:
|
||||
self.log.Warn(' ### nothing was found on kinopoisk for media name "%s"' % mediaName)
|
||||
return results
|
||||
|
||||
# Страница получена, берем с нее перечень всех названий фильмов.
|
||||
self.log.Debug('**** Finding %s (%s) got a KinoPoisk...' % (mediaName, mediaYear))
|
||||
|
||||
reobj = re.compile(r'<span><a href="http://m\.kinopoisk\.ru/movie/(\d+)/">(.+?)</a><br />(.+?)</span>')
|
||||
result = reobj.findall(page)
|
||||
|
||||
# Inspect query results titles and score them.
|
||||
self.log.Debug('found %d results (div info tags)' % len(result))
|
||||
itemIndex = -1
|
||||
for itemKinoPoiskId, itemTitleitemYear, itemAltTitle in result:
|
||||
itemIndex = itemIndex + 1
|
||||
itemAltTitle = itemAltTitle.replace(' ', '')
|
||||
try:
|
||||
itemTitle, itemYear = re.compile('^(.+?), (\d\d\d\d)$').findall(itemTitleitemYear.strip())[0]
|
||||
except:
|
||||
itemYear = None
|
||||
itemTitle = itemTitleitemYear
|
||||
itemScore = common.scoreMediaTitleMatch(mediaName, mediaYear, itemTitle, itemAltTitle, itemYear, itemIndex)
|
||||
results.append([itemKinoPoiskId, itemTitle, itemYear, itemScore])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Global definitions for KinoPoiskRu plugin.
|
||||
# Copyright (C) 2013 Yevgeny Nyden
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# @author zhenya (Yevgeny Nyden)
|
||||
# @version 1.52
|
||||
# @revision 148
|
||||
|
||||
# Default plugin preferences. When modifying, please also change
|
||||
# corresponding values in the ../DefaultPrefs.json file.
|
||||
KINOPOISK_PREF_DEFAULT_MAX_POSTERS = 1
|
||||
KINOPOISK_PREF_DEFAULT_MAX_ART = 2
|
||||
KINOPOISK_PREF_DEFAULT_GET_ALL_ACTORS = False
|
||||
KINOPOISK_PREF_DEFAULT_IMDB_SUPPORT = True
|
||||
KINOPOISK_PREF_DEFAULT_IMDB_RATING = False
|
||||
KINOPOISK_PREF_DEFAULT_KP_RATING = False
|
||||
|
||||
ENCODING_KINOPOISK_PAGE = 'cp1251'
|
||||
|
||||
# Разные страницы сайта.
|
||||
KINOPOISK_SITE_BASE = 'http://www.kinopoisk.ru/'
|
||||
KINOPOISK_RESOURCE_BASE = 'http://st.kinopoisk.ru/'
|
||||
KINOPOISK_TITLE_PAGE_URL = KINOPOISK_SITE_BASE + 'film/%s/'
|
||||
KINOPOISK_CAST_PAGE_URL = KINOPOISK_SITE_BASE + 'film/%s/cast/'
|
||||
KINOPOISK_STUDIO_PAGE_URL = KINOPOISK_SITE_BASE + 'film/%s/studio/'
|
||||
KINOPOISK_THUMBNAIL_BIG_URL = KINOPOISK_RESOURCE_BASE + 'images/film_big/%s.jpg'
|
||||
KINOPOISK_THUMBNAIL_SMALL_URL = KINOPOISK_RESOURCE_BASE + 'images/film/%s.jpg'
|
||||
KINOPOISK_POSTERS_URL = KINOPOISK_SITE_BASE + 'film/%s/posters/page/%d/'
|
||||
KINOPOISK_STILLS_URL = KINOPOISK_SITE_BASE + 'film/%s/stills/page/%d/'
|
||||
|
||||
# Страница поиска.
|
||||
KINOPOISK_SEARCH = KINOPOISK_SITE_BASE + 'index.php?first=no&kp_query=%s'
|
||||
KINOPOISK_SEARCH_SIMPLE = 'http://m.kinopoisk.ru/search/%s/'
|
||||
@@ -0,0 +1,288 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- test-case-name: pytils.test.test_translit -*-
|
||||
# pytils - simple processing for russian strings
|
||||
# Copyright (C) 2006-2007 Yury Yurevich
|
||||
#
|
||||
# http://www.pyobject.ru/projects/pytils/
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation, version 2
|
||||
# of the License.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
"""
|
||||
Simple transliteration
|
||||
"""
|
||||
|
||||
# __id__ = __revision__ = "$Id: translit.py 102 2007-07-12 12:33:36Z the.pythy $"
|
||||
#__url__ = "$URL: https://pythy.googlecode.com/svn/tags/pytils/0_2_2/pytils/translit.py $"
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
TRANSTABLE = (
|
||||
(u"'", u"'"),
|
||||
(u'"', u'"'),
|
||||
(u"‘", u"'"),
|
||||
(u"’", u"'"),
|
||||
(u"«", u'"'),
|
||||
(u"»", u'"'),
|
||||
(u"–", u"-"),
|
||||
(u"…", u"..."),
|
||||
(u"№", u"#"),
|
||||
## верхний регистр
|
||||
# трехбуквенные замены
|
||||
(u"Щ", u"Sch"),
|
||||
# при замене русский->английский будет первая замена,
|
||||
# т.е. Sch
|
||||
# а вот если английский->русский, то вариант SCH и Sch --
|
||||
# оба пройдут
|
||||
(u"Щ", u"SCH"),
|
||||
# двухбуквенные замены
|
||||
(u"Ё", u"Yo"),
|
||||
(u"Ё", u"YO"),
|
||||
(u"Ж", u"Zh"),
|
||||
(u"Ж", u"ZH"),
|
||||
(u"Ц", u"Ts"),
|
||||
(u"Ц", u"TS"),
|
||||
(u"Ч", u"Ch"),
|
||||
(u"Ч", u"CH"),
|
||||
(u"Ш", u"Sh"),
|
||||
(u"Ш", u"SH"),
|
||||
(u"Ы", u"Yi"),
|
||||
(u"Ы", u"YI"),
|
||||
(u"Ю", u"Yu"),
|
||||
(u"Ю", u"YU"),
|
||||
(u"Я", u"Ya"),
|
||||
(u"Я", u"YA"),
|
||||
(u"ИЙ", u"IY"),
|
||||
# однобуквенные замены
|
||||
(u"А", u"A"),
|
||||
(u"Б", u"B"),
|
||||
(u"В", u"V"),
|
||||
(u"Г", u"G"),
|
||||
(u"Д", u"D"),
|
||||
(u"Е", u"E"),
|
||||
(u"З", u"Z"),
|
||||
(u"И", u"I"),
|
||||
(u"Й", u"J"),
|
||||
(u"К", u"K"),
|
||||
(u"Л", u"L"),
|
||||
(u"М", u"M"),
|
||||
(u"Н", u"N"),
|
||||
(u"О", u"O"),
|
||||
(u"П", u"P"),
|
||||
(u"Р", u"R"),
|
||||
(u"С", u"S"),
|
||||
(u"Т", u"T"),
|
||||
(u"У", u"U"),
|
||||
(u"Ф", u"F"),
|
||||
(u"Х", u"H"),
|
||||
(u"Э", u"E"),
|
||||
(u"Ъ", u"`"),
|
||||
(u"Ы", u"Y"),
|
||||
(u"Ь", u"'"),
|
||||
## нижний регистр
|
||||
# трехбуквенные замены
|
||||
(u"щ", u"sch"),
|
||||
# двухбуквенные замены
|
||||
(u"ё", u"yo"),
|
||||
(u"ж", u"zh"),
|
||||
(u"ц", u"ts"),
|
||||
(u"ч", u"ch"),
|
||||
(u"ш", u"sh"),
|
||||
(u"ы", u"yi"),
|
||||
(u"ю", u"yu"),
|
||||
(u"я", u"ya"),
|
||||
(u"я", u"ja"),
|
||||
(u"ий", u"iy"),
|
||||
# однобуквенные замены
|
||||
(u"а", u"a"),
|
||||
(u"б", u"b"),
|
||||
(u"в", u"v"),
|
||||
(u"г", u"g"),
|
||||
(u"д", u"d"),
|
||||
(u"е", u"e"),
|
||||
(u"з", u"z"),
|
||||
(u"и", u"i"),
|
||||
(u"й", u"j"),
|
||||
(u"к", u"k"),
|
||||
(u"л", u"l"),
|
||||
(u"м", u"m"),
|
||||
(u"н", u"n"),
|
||||
(u"о", u"o"),
|
||||
(u"п", u"p"),
|
||||
(u"р", u"r"),
|
||||
(u"с", u"s"),
|
||||
(u"т", u"t"),
|
||||
(u"у", u"u"),
|
||||
(u"ф", u"f"),
|
||||
(u"х", u"h"),
|
||||
(u"э", u"e"),
|
||||
(u"ъ", u"`"),
|
||||
(u"ь", u"'"),
|
||||
# для полноты английского алфавит (в slugify)
|
||||
# дополняем английскими буквами, которых
|
||||
# не в парах
|
||||
(u"c", u"c"),
|
||||
(u"q", u"q"),
|
||||
(u"y", u"y"),
|
||||
(u"x", u"x"),
|
||||
(u"w", u"w"),
|
||||
(u"1", u"1"),
|
||||
(u"2", u"2"),
|
||||
(u"3", u"3"),
|
||||
(u"4", u"4"),
|
||||
(u"5", u"5"),
|
||||
(u"6", u"6"),
|
||||
(u"7", u"7"),
|
||||
(u"8", u"8"),
|
||||
(u"9", u"9"),
|
||||
(u"0", u"0"),
|
||||
) #: Translation table
|
||||
|
||||
RU_ALPHABET = [x[0] for x in TRANSTABLE] #: Russian alphabet that we can translate
|
||||
EN_ALPHABET = [x[1] for x in TRANSTABLE] #: English alphabet that we can detransliterate
|
||||
ALPHABET = RU_ALPHABET + EN_ALPHABET #: Alphabet that we can (de)transliterate
|
||||
|
||||
|
||||
def translify(in_string):
|
||||
"""
|
||||
Translify russian text
|
||||
|
||||
@param in_string: input string
|
||||
@type in_string: C{unicode}
|
||||
|
||||
@return: transliterated string
|
||||
@rtype: C{str}
|
||||
|
||||
@raise TypeError: when in_string is not C{unicode}
|
||||
@raise ValueError: when string doesn't transliterate completely
|
||||
"""
|
||||
if not isinstance(in_string, unicode):
|
||||
raise TypeError("Argument must be unicode, not %s" % type(in_string))
|
||||
|
||||
translit = in_string
|
||||
for symb_in, symb_out in TRANSTABLE:
|
||||
translit = translit.replace(symb_in, symb_out)
|
||||
|
||||
try:
|
||||
translit = str(translit)
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError("Unicode string doesn't transliterate completely, " + \
|
||||
"is it russian?")
|
||||
|
||||
return translit
|
||||
|
||||
|
||||
def detranslify(in_string):
|
||||
"""
|
||||
Detranslify
|
||||
|
||||
@param in_string: input string
|
||||
@type in_string: C{basestring}
|
||||
|
||||
@return: detransliterated string
|
||||
@rtype: C{str}
|
||||
|
||||
@raise TypeError: when in_string neither C{str}, no C{unicode}
|
||||
@raise ValueError: if in_string is C{str}, but it isn't ascii
|
||||
"""
|
||||
if not isinstance(in_string, basestring):
|
||||
raise TypeError("Argument must be basestring, not %s" % type(in_string))
|
||||
|
||||
# в unicode
|
||||
try:
|
||||
russian = unicode(in_string)
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("We expects when in_string is str type," + \
|
||||
"it is an ascii, but now it isn't. Use unicode " + \
|
||||
"in this case.")
|
||||
|
||||
for symb_out, symb_in in TRANSTABLE:
|
||||
russian = russian.replace(symb_in, symb_out)
|
||||
|
||||
return russian
|
||||
|
||||
|
||||
def slugify(in_string):
|
||||
"""
|
||||
Prepare string for slug (i.e. URL or file/dir name)
|
||||
|
||||
@param in_string: input string
|
||||
@type in_string: C{basestring}
|
||||
|
||||
@return: slug-string
|
||||
@rtype: C{str}
|
||||
|
||||
@raise TypeError: when in_string isn't C{unicode} or C{str}
|
||||
@raise ValueError: if in_string is C{str}, but it isn't ascii
|
||||
"""
|
||||
if not isinstance(in_string, basestring):
|
||||
raise TypeError("Argument must be basestring, not %s" % type(in_string))
|
||||
try:
|
||||
u_in_string = unicode(in_string).lower()
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("We expects when in_string is str type," + \
|
||||
"it is an ascii, but now it isn't. Use unicode " + \
|
||||
"in this case.")
|
||||
# convert & to "and"
|
||||
u_in_string = re.sub('\&\;|\&', ' and ', u_in_string)
|
||||
# replace spaces by hyphen
|
||||
u_in_string = re.sub('[-\s]+', '-', u_in_string)
|
||||
# remove symbols that not in alphabet
|
||||
u_in_string = u''.join([symb for symb in u_in_string if symb in ALPHABET])
|
||||
# translify it
|
||||
out_string = translify(u_in_string)
|
||||
# remove non-alpha
|
||||
return re.sub('[^\w\s-]', '', out_string).strip().lower()
|
||||
|
||||
|
||||
def dirify(in_string):
|
||||
"""
|
||||
Alias for L{slugify}
|
||||
"""
|
||||
slugify(in_string)
|
||||
|
||||
|
||||
def provide_unicode(stext, encoding, default=u"неизвестно"):
|
||||
"""
|
||||
Provide Unicode from text
|
||||
|
||||
@param stext: text
|
||||
@type stext: C{str}
|
||||
|
||||
@param encoding: encoding if input text
|
||||
@type encoding: C{str}
|
||||
|
||||
@return: C{unicode}
|
||||
"""
|
||||
try:
|
||||
utext = str(stext).decode(encoding)
|
||||
except UnicodeDecodeError, err:
|
||||
utext = default % {'error': err, 'value': u""}
|
||||
return utext
|
||||
|
||||
|
||||
def provide_str(utext, encoding, default="unknown"):
|
||||
"""
|
||||
Provide text from Unicode
|
||||
|
||||
@param utext: unicode text
|
||||
@type utext: C{unicode}
|
||||
|
||||
@param encoding: encoding of output text
|
||||
@type encoding: C{str}
|
||||
|
||||
@return: C{str}
|
||||
"""
|
||||
try:
|
||||
stext = unicode(utext).encode(encoding)
|
||||
except UnicodeEncodeError, err:
|
||||
stext = default % {'error': err, 'value': ""}
|
||||
return stext
|
||||
@@ -0,0 +1,565 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import time
|
||||
import urllib
|
||||
|
||||
from net import HTTP
|
||||
from cache import Cache
|
||||
from html import Clear
|
||||
import kinopoisk.LOGGER
|
||||
import kinopoisk.pageparser
|
||||
import kinopoisk.common
|
||||
|
||||
|
||||
GENRE = {
|
||||
'anime': 1750,
|
||||
'biography': 22,
|
||||
'action': 3,
|
||||
'western': 13,
|
||||
'military': 19,
|
||||
'detective': 17,
|
||||
'children': 456,
|
||||
'for adults': 20,
|
||||
'documentary': 12,
|
||||
'drama': 8,
|
||||
'game': 27,
|
||||
'history': 23,
|
||||
'comedy': 6,
|
||||
'concert': 1747,
|
||||
'short': 15,
|
||||
'criminal': 16,
|
||||
'romance': 7,
|
||||
'music': 21,
|
||||
'cartoon': 14,
|
||||
'musical': 9,
|
||||
'news': 28,
|
||||
'adventures': 10,
|
||||
'realitytv': 25,
|
||||
'family': 11,
|
||||
'sports': 24,
|
||||
'talk shows': 26,
|
||||
'thriller': 4,
|
||||
'horror': 1,
|
||||
'fiction': 2,
|
||||
'filmnoir': 18,
|
||||
'fantasy': 5
|
||||
}
|
||||
|
||||
COUNTRIES = (
|
||||
(0, u'Все'),
|
||||
(2, u'Россия'),
|
||||
(1, u'США'),
|
||||
(13, u'СССР'),
|
||||
(25, u'Австралия'),
|
||||
(57, u'Австрия'),
|
||||
(136, u'Азербайджан'),
|
||||
(120, u'Албания'),
|
||||
(20, u'Алжир'),
|
||||
(1026, u'Американские Виргинские острова'),
|
||||
(139, u'Ангола'),
|
||||
(159, u'Андорра'),
|
||||
(1044, u'Антарктида'),
|
||||
(1030, u'Антигуа и Барбуда'),
|
||||
(1009, u'Антильские Острова'),
|
||||
(24, u'Аргентина'),
|
||||
(89, u'Армения'),
|
||||
(175, u'Аруба'),
|
||||
(113, u'Афганистан'),
|
||||
(124, u'Багамы'),
|
||||
(75, u'Бангладеш'),
|
||||
(105, u'Барбадос'),
|
||||
(164, u'Бахрейн'),
|
||||
(69, u'Беларусь'),
|
||||
(173, u'Белиз'),
|
||||
(41, u'Бельгия'),
|
||||
(140, u'Бенин'),
|
||||
(109, u'Берег Слоновой кости'),
|
||||
(1004, u'Бермуды'),
|
||||
(148, u'Бирма'),
|
||||
(63, u'Болгария'),
|
||||
(118, u'Боливия'),
|
||||
(178, u'Босния'),
|
||||
(39, u'Босния-Герцеговина'),
|
||||
(145, u'Ботсвана'),
|
||||
(10, u'Бразилия'),
|
||||
(92, u'Буркина-Фасо'),
|
||||
(162, u'Бурунди'),
|
||||
(114, u'Бутан'),
|
||||
(1059, u'Вануату'),
|
||||
(11, u'Великобритания'),
|
||||
(49, u'Венгрия'),
|
||||
(72, u'Венесуэла'),
|
||||
(1043, u'Восточная Сахара'),
|
||||
(52, u'Вьетнам'),
|
||||
(170, u'Вьетнам Северный'),
|
||||
(127, u'Габон'),
|
||||
(99, u'Гаити'),
|
||||
(165, u'Гайана'),
|
||||
(1040, u'Гамбия'),
|
||||
(144, u'Гана'),
|
||||
(135, u'Гватемала'),
|
||||
(129, u'Гвинея'),
|
||||
(116, u'Гвинея-Бисау'),
|
||||
(3, u'Германия'),
|
||||
(60, u'Германия (ГДР)'),
|
||||
(18, u'Германия (ФРГ)'),
|
||||
(1022, u'Гибралтар'),
|
||||
(112, u'Гондурас'),
|
||||
(28, u'Гонконг'),
|
||||
(117, u'Гренландия'),
|
||||
(55, u'Греция'),
|
||||
(61, u'Грузия'),
|
||||
(142, u'Гуаделупа'),
|
||||
(1045, u'Гуам'),
|
||||
(4, u'Дания'),
|
||||
(1037, u'Демократическая Республика Конго'),
|
||||
(1028, u'Джибути'),
|
||||
(1031, u'Доминика'),
|
||||
(128, u'Доминикана'),
|
||||
(101, u'Египет'),
|
||||
(155, u'Заир'),
|
||||
(133, u'Замбия'),
|
||||
(104, u'Зимбабве'),
|
||||
(42, u'Израиль'),
|
||||
(29, u'Индия'),
|
||||
(73, u'Индонезия'),
|
||||
(154, u'Иордания'),
|
||||
(90, u'Ирак'),
|
||||
(48, u'Иран'),
|
||||
(38, u'Ирландия'),
|
||||
(37, u'Исландия'),
|
||||
(15, u'Испания'),
|
||||
(14, u'Италия'),
|
||||
(169, u'Йемен'),
|
||||
(146, u'Кабо-Верде'),
|
||||
(122, u'Казахстан'),
|
||||
(1051, u'Каймановы острова'),
|
||||
(84, u'Камбоджа'),
|
||||
(95, u'Камерун'),
|
||||
(6, u'Канада'),
|
||||
(1002, u'Катар'),
|
||||
(100, u'Кения'),
|
||||
(64, u'Кипр'),
|
||||
(1024, u'Кирибати'),
|
||||
(31, u'Китай'),
|
||||
(56, u'Колумбия'),
|
||||
(1058, u'Коморы'),
|
||||
(134, u'Конго'),
|
||||
(1014, u'Конго (ДРК)'),
|
||||
(156, u'Корея'),
|
||||
(137, u'Корея Северная'),
|
||||
(26, u'Корея Южная'),
|
||||
(1013, u'Косово'),
|
||||
(131, u'Коста-Рика'),
|
||||
(76, u'Куба'),
|
||||
(147, u'Кувейт'),
|
||||
(86, u'Кыргызстан'),
|
||||
(149, u'Лаос'),
|
||||
(54, u'Латвия'),
|
||||
(1015, u'Лесото'),
|
||||
(176, u'Либерия'),
|
||||
(97, u'Ливан'),
|
||||
(126, u'Ливия'),
|
||||
(123, u'Литва'),
|
||||
(125, u'Лихтенштейн'),
|
||||
(59, u'Люксембург'),
|
||||
(115, u'Маврикий'),
|
||||
(67, u'Мавритания'),
|
||||
(150, u'Мадагаскар'),
|
||||
(153, u'Макао'),
|
||||
(80, u'Македония'),
|
||||
(1025, u'Малави'),
|
||||
(83, u'Малайзия'),
|
||||
(151, u'Мали'),
|
||||
(1050, u'Мальдивы'),
|
||||
(111, u'Мальта'),
|
||||
(43, u'Марокко'),
|
||||
(102, u'Мартиника'),
|
||||
(1042, u'Масаи'),
|
||||
(17, u'Мексика'),
|
||||
(1041, u'Мелкие отдаленные острова США'),
|
||||
(81, u'Мозамбик'),
|
||||
(58, u'Молдова'),
|
||||
(22, u'Монако'),
|
||||
(132, u'Монголия'),
|
||||
(1034, u'Мьянма'),
|
||||
(91, u'Намибия'),
|
||||
(106, u'Непал'),
|
||||
(157, u'Нигер'),
|
||||
(110, u'Нигерия'),
|
||||
(12, u'Нидерланды'),
|
||||
(138, u'Никарагуа'),
|
||||
(35, u'Новая Зеландия'),
|
||||
(1006, u'Новая Каледония'),
|
||||
(33, u'Норвегия'),
|
||||
(119, u'ОАЭ'),
|
||||
(1019, u'Оккупированная Палестинская территория'),
|
||||
(1003, u'Оман'),
|
||||
(1052, u'Остров Мэн'),
|
||||
(1047, u'Остров Святой Елены'),
|
||||
(1007, u'острова Теркс и Кайкос'),
|
||||
(74, u'Пакистан'),
|
||||
(1057, u'Палау'),
|
||||
(78, u'Палестина'),
|
||||
(107, u'Панама'),
|
||||
(163, u'Папуа - Новая Гвинея'),
|
||||
(143, u'Парагвай'),
|
||||
(23, u'Перу'),
|
||||
(32, u'Польша'),
|
||||
(36, u'Португалия'),
|
||||
(82, u'Пуэрто Рико'),
|
||||
(1036, u'Реюньон'),
|
||||
(1033, u'Российская империя'),
|
||||
(2, u'Россия'),
|
||||
(103, u'Руанда'),
|
||||
(46, u'Румыния'),
|
||||
(121, u'Сальвадор'),
|
||||
(1039, u'Самоа'),
|
||||
(1011, u'Сан-Марино'),
|
||||
(158, u'Саудовская Аравия'),
|
||||
(1029, u'Свазиленд'),
|
||||
(1010, u'Сейшельские острова'),
|
||||
(65, u'Сенегал'),
|
||||
(1055, u'Сент-Винсент и Гренадины'),
|
||||
(1049, u'Сент-Люсия'),
|
||||
(177, u'Сербия'),
|
||||
(174, u'Сербия и Черногория'),
|
||||
(1021, u'Сиам'),
|
||||
(45, u'Сингапур'),
|
||||
(98, u'Сирия'),
|
||||
(94, u'Словакия'),
|
||||
(40, u'Словения'),
|
||||
(160, u'Сомали'),
|
||||
(13, u'СССР'),
|
||||
(167, u'Судан'),
|
||||
(171, u'Суринам'),
|
||||
(1, u'США'),
|
||||
(1023, u'Сьерра-Леоне'),
|
||||
(70, u'Таджикистан'),
|
||||
(44, u'Таиланд'),
|
||||
(27, u'Тайвань'),
|
||||
(130, u'Танзания'),
|
||||
(161, u'Того'),
|
||||
(1012, u'Тонго'),
|
||||
(88, u'Тринидад и Тобаго'),
|
||||
(1053, u'Тувалу'),
|
||||
(50, u'Тунис'),
|
||||
(152, u'Туркменистан'),
|
||||
(68, u'Турция'),
|
||||
(172, u'Уганда'),
|
||||
(71, u'Узбекистан'),
|
||||
(62, u'Украина'),
|
||||
(79, u'Уругвай'),
|
||||
(1008, u'Фарерские острова'),
|
||||
(1038, u'Федеративные Штаты Микронезии'),
|
||||
(166, u'Фиджи'),
|
||||
(47, u'Филиппины'),
|
||||
(7, u'Финляндия'),
|
||||
(8, u'Франция'),
|
||||
(1032, u'Французская Гвиана'),
|
||||
(1046, u'Французская Полинезия'),
|
||||
(85, u'Хорватия'),
|
||||
(141, u'ЦАР'),
|
||||
(77, u'Чад'),
|
||||
(1020, u'Черногория'),
|
||||
(34, u'Чехия'),
|
||||
(16, u'Чехословакия'),
|
||||
(51, u'Чили'),
|
||||
(21, u'Швейцария'),
|
||||
(5, u'Швеция'),
|
||||
(108, u'Шри-Ланка'),
|
||||
(96, u'Эквадор'),
|
||||
(87, u'Эритрея'),
|
||||
(53, u'Эстония'),
|
||||
(168, u'Эфиопия'),
|
||||
(30, u'ЮАР'),
|
||||
(19, u'Югославия'),
|
||||
(66, u'Югославия (ФР)'),
|
||||
(93, u'Ямайка'),
|
||||
(9, u'Япония')
|
||||
)
|
||||
|
||||
|
||||
class KinoPoisk:
|
||||
"""
|
||||
|
||||
API:
|
||||
scraper - скрапер
|
||||
movie - профайл фильма
|
||||
search - поиск фильма
|
||||
best - поиск лучших фильмов
|
||||
person - поиск персон
|
||||
work - информация о работах персоны
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, language='ru'):
|
||||
dbname='kinopoisk.%s.db' % language
|
||||
self.cache = Cache(dbname, 1.0)
|
||||
self.html = Clear()
|
||||
|
||||
self.timeout = 60.0
|
||||
|
||||
self.http = HTTP()
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Referer': 'http://www.kinopoisk.ru/level/7/'
|
||||
}
|
||||
|
||||
# API
|
||||
|
||||
def scraper(self, search, year=None):
|
||||
|
||||
try:
|
||||
if not isinstance(search, list):
|
||||
search = [search]
|
||||
tag = 'scraper:' + urllib.quote_plus(":".join(search).encode('utf8'))
|
||||
except:
|
||||
return None
|
||||
else:
|
||||
|
||||
if year:
|
||||
tag += ':' + str(year)
|
||||
|
||||
id = self.cache.get(tag, self._scraper, search, year)
|
||||
if not id:
|
||||
return None
|
||||
|
||||
return self.movie(id)
|
||||
|
||||
def movie(self, id):
|
||||
id = str(id)
|
||||
return self.cache.get('movie:' + id, self._movie, id)
|
||||
|
||||
def search(self, search, year):
|
||||
return self._search_movie(search, year)
|
||||
|
||||
def countries(self):
|
||||
return COUNTRIES
|
||||
|
||||
def country(self, id, default=None):
|
||||
country = [x[1] for x in COUNTRIES if x[0] == id]
|
||||
return country[0] if country else default
|
||||
|
||||
def _search_movie(self, search, year=None):
|
||||
parser = kinopoisk.pageparser.PageParser(kinopoisk.LOGGER, isDebug=True)
|
||||
orginalname = search[0]
|
||||
if len(search) > 1:
|
||||
name = search[1]
|
||||
else:
|
||||
name = None
|
||||
results = parser.fetchAndParseSearchResults(orginalname, year, name)
|
||||
if results and results[0][3] > 70:
|
||||
return results[0][0]
|
||||
|
||||
def _scraper(self, search, year):
|
||||
timeout = True
|
||||
|
||||
# если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте)
|
||||
if year and year > time.gmtime(time.time()).tm_year:
|
||||
timeout = 7 * 24 * 60 * 60 * 4 # 4 week
|
||||
|
||||
movie_id = self._search_movie(search, year)
|
||||
|
||||
if movie_id is None:
|
||||
# сохраняем пустой результат на 4 week
|
||||
return 7 * 24 * 60 * 60 * 4, None
|
||||
|
||||
else:
|
||||
return timeout, movie_id
|
||||
|
||||
def _movie(self, id):
|
||||
response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/', headers=self.headers,
|
||||
timeout=self.timeout)
|
||||
if response.error:
|
||||
return False, None
|
||||
|
||||
html = response.body.decode('windows-1251')
|
||||
|
||||
res = {
|
||||
'icon': None,
|
||||
'thumbnail': None,
|
||||
'properties': {
|
||||
'fanart_image': None,
|
||||
},
|
||||
'info': {
|
||||
'count': int(id)
|
||||
}
|
||||
}
|
||||
|
||||
# имя, оригинальное имя, девиз, цензура, год, top250
|
||||
# runtime - длительность фильма (в отдельную переменную, иначе не видно размер файла)
|
||||
for tag, reg, cb in (
|
||||
('title', '<title>(.+?)</title>', self.html.string),
|
||||
('originaltitle', 'itemprop="alternativeHeadline">([^<]*)</span>', self.html.string),
|
||||
('tagline', '<td style="color\: #555">«(.+?)»</td></tr>', self.html.string),
|
||||
('mpaa', 'images/mpaa/([^\.]+).gif', self.html.string),
|
||||
('runtime', '<td class="time" id="runtime">[^<]+<span style="color\: #999">/</span>([^<]+)</td>',
|
||||
self.html.string),
|
||||
('year', '<a href="/lists/m_act%5Byear%5D/([0-9]+)/"', int),
|
||||
('top250', 'Топ250\: <a\shref="/level/20/#([0-9]+)', int)
|
||||
|
||||
):
|
||||
r = re.compile(reg, re.U).search(html)
|
||||
if r:
|
||||
value = r.group(1).strip()
|
||||
if value:
|
||||
res['info'][tag] = cb(value)
|
||||
|
||||
|
||||
# режисеры, сценаристы, жанры
|
||||
for tag, reg in (
|
||||
('director', u'<td itemprop="director">(.+?)</td>'),
|
||||
('writer', u'<td class="type">сценарий</td><td[^>]*>(.+?)</td>'),
|
||||
('genre', u'<span itemprop="genre">(.+?)</span>')
|
||||
):
|
||||
r = re.compile(reg, re.U | re.S).search(html)
|
||||
if r:
|
||||
r2 = []
|
||||
for r in re.compile('<a href="[^"]+">([^<]+)</a>', re.U).findall(r.group(1)):
|
||||
r = self.html.string(r)
|
||||
if r and r != '...':
|
||||
r2.append(r)
|
||||
if r2:
|
||||
res['info'][tag] = u', '.join(r2)
|
||||
|
||||
# актеры
|
||||
r = re.compile(u'<h4>В главных ролях:</h4>(.+?)</ul>', re.U | re.S).search(html)
|
||||
if r:
|
||||
actors = []
|
||||
for r in re.compile('<li itemprop="actors"><a [^>]+>([^<]+)</a></li>', re.U).findall(r.group(1)):
|
||||
r = self.html.string(r)
|
||||
if r and r != '...':
|
||||
actors.append(r)
|
||||
if actors:
|
||||
res['info']['cast'] = actors[:]
|
||||
# res['info']['castandrole'] = actors[:]
|
||||
|
||||
# описание фильма
|
||||
r = re.compile('<span class="_reachbanner_"><div class="brand_words" itemprop="description">(.+?)</div></span>',
|
||||
re.U).search(html)
|
||||
if r:
|
||||
plot = self.html.text(r.group(1).replace('<=end=>', '\n'))
|
||||
if plot:
|
||||
res['info']['plot'] = plot
|
||||
|
||||
# IMDB
|
||||
r = re.compile('IMDb: ([0-9.]+) \(([0-9\s]+)\)</div>', re.U).search(html)
|
||||
if r:
|
||||
res['info']['rating'] = float(r.group(1).strip())
|
||||
res['info']['votes'] = r.group(2).strip()
|
||||
|
||||
|
||||
# премьера
|
||||
r = re.compile(u'премьера \(мир\)</td>(.+?)</tr>', re.U | re.S).search(html)
|
||||
if r:
|
||||
r = re.compile(u'data\-ical\-date="([^"]+)"', re.U | re.S).search(r.group(1))
|
||||
if r:
|
||||
data = r.group(1).split(' ')
|
||||
if len(data) == 3:
|
||||
i = 0
|
||||
for mon in (
|
||||
u'января', u'февраля', u'марта', u'апреля', u'мая', u'июня', u'июля', u'августа', u'сентября',
|
||||
u'октября', u'ноября', u'декабря'):
|
||||
i += 1
|
||||
if mon == data[1]:
|
||||
mon = str(i)
|
||||
if len(mon) == 1:
|
||||
mon = '0' + mon
|
||||
day = data[0]
|
||||
if len(day) == 1:
|
||||
day = '0' + day
|
||||
res['info']['premiered'] = '-'.join([data[2], mon, day])
|
||||
break
|
||||
|
||||
|
||||
# постер
|
||||
r = re.compile(u'onclick="openImgPopup\(([^\)]+)\)', re.U | re.S).search(html)
|
||||
if r:
|
||||
poster = r.group(1).replace("'", '').strip()
|
||||
if poster:
|
||||
res['thumbnail'] = res['icon'] = 'http://kinopoisk.ru' + poster
|
||||
|
||||
menu = re.compile('<ul id="newMenuSub" class="clearfix(.+?)<!\-\- /menu \-\->', re.U | re.S).search(html)
|
||||
if menu:
|
||||
menu = menu.group(1)
|
||||
|
||||
# фанарт
|
||||
if menu.find('/film/' + id + '/wall/') != -1:
|
||||
response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/wall/', headers=self.headers,
|
||||
timeout=self.timeout)
|
||||
if not response.error:
|
||||
html = response.body.decode('windows-1251')
|
||||
fanart = re.compile('<a href="/picture/([0-9]+)/w_size/([0-9]+)/">', re.U).findall(html)
|
||||
if fanart:
|
||||
fanart.sort(cmp=lambda (id1, size1), (id2, size2): cmp(int(size1), int(size2)))
|
||||
|
||||
# пробуем взять максимально подходящее
|
||||
fanart_best = [x for x in fanart if int(x[1]) <= 1280]
|
||||
if fanart_best:
|
||||
fanart = fanart_best
|
||||
|
||||
response = self.http.fetch(
|
||||
'http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/w_size/' + fanart[-1][1] + '/',
|
||||
headers=self.headers, timeout=self.timeout)
|
||||
if not response.error:
|
||||
html = response.body.decode('windows-1251')
|
||||
r = re.compile('id="image" src="([^"]+)"', re.U | re.S).search(html)
|
||||
if r:
|
||||
res['properties']['fanart_image'] = r.group(1).strip()
|
||||
|
||||
|
||||
# если нет фанарта (обоев), то пробуем получить кадры
|
||||
if not res['properties']['fanart_image'] and menu.find('/film/' + id + '/stills/') != -1:
|
||||
response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/stills/', headers=self.headers,
|
||||
timeout=self.timeout)
|
||||
if not response.error:
|
||||
html = response.body.decode('windows-1251')
|
||||
fanart = re.compile(
|
||||
'<a href="/picture/([0-9]+)/"><img src="[^<]+</a>[^<]+<b><i>([0-9]+)×([0-9]+)</i>',
|
||||
re.U).findall(html)
|
||||
if fanart:
|
||||
fanart.sort(cmp=lambda (id1, size1, t1), (id2, size2, t2): cmp(int(size1), int(size2)))
|
||||
|
||||
# пробуем взять максимально подходящее
|
||||
fanart_best = [x for x in fanart if int(x[1]) <= 1280 and int(x[1]) > int(x[2])]
|
||||
if fanart_best:
|
||||
fanart = fanart_best
|
||||
|
||||
response = self.http.fetch('http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/',
|
||||
headers=self.headers, timeout=self.timeout)
|
||||
if not response.error:
|
||||
html = response.body.decode('windows-1251')
|
||||
r = re.compile('id="image" src="([^"]+)"', re.U | re.S).search(html)
|
||||
if r:
|
||||
res['properties']['fanart_image'] = r.group(1).strip()
|
||||
|
||||
|
||||
# студии
|
||||
if menu.find('/film/' + id + '/studio/') != -1:
|
||||
response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/studio/', headers=self.headers,
|
||||
timeout=self.timeout)
|
||||
if not response.error:
|
||||
html = response.body.decode('windows-1251')
|
||||
r = re.compile(u'<b>Производство:</b>(.+?)</table>', re.U | re.S).search(html)
|
||||
if r:
|
||||
studio = []
|
||||
for r in re.compile('<a href="/lists/m_act%5Bstudio%5D/[0-9]+/" class="all">(.+?)</a>',
|
||||
re.U).findall(r.group(1)):
|
||||
r = self.html.string(r)
|
||||
if r:
|
||||
studio.append(r)
|
||||
if studio:
|
||||
res['info']['studio'] = u', '.join(studio)
|
||||
|
||||
timeout = True
|
||||
# если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте)
|
||||
if 'year' not in res['info'] or not res['properties']['fanart_image'] \
|
||||
or int(res['info']['year']) > time.gmtime(time.time()).tm_year:
|
||||
timeout = 7 * 24 * 60 * 60 * 4 # 4 week
|
||||
|
||||
return timeout, res
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
import cookielib
|
||||
import base64
|
||||
import mimetools
|
||||
import itertools
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
|
||||
|
||||
RE = {
|
||||
'content-disposition': re.compile('attachment;\sfilename="*([^"\s]+)"|\s')
|
||||
}
|
||||
|
||||
# ################################
|
||||
#
|
||||
# HTTP
|
||||
#
|
||||
# ################################
|
||||
|
||||
class HTTP:
|
||||
def __init__(self):
|
||||
self._dirname = xbmc.translatePath('special://temp')
|
||||
for subdir in ('xbmcup', 'plugin.video.torrenter'):
|
||||
self._dirname = os.path.join(self._dirname, subdir)
|
||||
if not xbmcvfs.exists(self._dirname):
|
||||
xbmcvfs.mkdir(self._dirname)
|
||||
|
||||
|
||||
def fetch(self, request, **kwargs):
|
||||
self.con, self.fd, self.progress, self.cookies, self.request = None, None, None, None, request
|
||||
|
||||
if not isinstance(self.request, HTTPRequest):
|
||||
self.request = HTTPRequest(url=self.request, **kwargs)
|
||||
|
||||
self.response = HTTPResponse(self.request)
|
||||
|
||||
xbmc.log('XBMCup: HTTP: request: ' + str(self.request), xbmc.LOGDEBUG)
|
||||
|
||||
try:
|
||||
self._opener()
|
||||
self._fetch()
|
||||
except Exception, e:
|
||||
xbmc.log('XBMCup: HTTP: ' + str(e), xbmc.LOGERROR)
|
||||
if isinstance(e, urllib2.HTTPError):
|
||||
self.response.code = e.code
|
||||
self.response.error = e
|
||||
else:
|
||||
self.response.code = 200
|
||||
|
||||
if self.fd:
|
||||
self.fd.close()
|
||||
self.fd = None
|
||||
|
||||
if self.con:
|
||||
self.con.close()
|
||||
self.con = None
|
||||
|
||||
if self.progress:
|
||||
self.progress.close()
|
||||
self.progress = None
|
||||
|
||||
self.response.time = time.time() - self.response.time
|
||||
|
||||
xbmc.log('XBMCup: HTTP: response: ' + str(self.response), xbmc.LOGDEBUG)
|
||||
|
||||
return self.response
|
||||
|
||||
|
||||
def _opener(self):
|
||||
|
||||
build = [urllib2.HTTPHandler()]
|
||||
|
||||
if self.request.redirect:
|
||||
build.append(urllib2.HTTPRedirectHandler())
|
||||
|
||||
if self.request.proxy_host and self.request.proxy_port:
|
||||
build.append(urllib2.ProxyHandler(
|
||||
{self.request.proxy_protocol: self.request.proxy_host + ':' + str(self.request.proxy_port)}))
|
||||
|
||||
if self.request.proxy_username:
|
||||
proxy_auth_handler = urllib2.ProxyBasicAuthHandler()
|
||||
proxy_auth_handler.add_password('realm', 'uri', self.request.proxy_username,
|
||||
self.request.proxy_password)
|
||||
build.append(proxy_auth_handler)
|
||||
|
||||
if self.request.cookies:
|
||||
self.request.cookies = os.path.join(self._dirname, self.request.cookies)
|
||||
self.cookies = cookielib.MozillaCookieJar()
|
||||
if os.path.isfile(self.request.cookies):
|
||||
self.cookies.load(self.request.cookies)
|
||||
build.append(urllib2.HTTPCookieProcessor(self.cookies))
|
||||
|
||||
urllib2.install_opener(urllib2.build_opener(*build))
|
||||
|
||||
|
||||
def _fetch(self):
|
||||
params = {} if self.request.params is None else self.request.params
|
||||
|
||||
if self.request.upload:
|
||||
boundary, upload = self._upload(self.request.upload, params)
|
||||
req = urllib2.Request(self.request.url)
|
||||
req.add_data(upload)
|
||||
else:
|
||||
|
||||
if self.request.method == 'POST':
|
||||
if isinstance(params, dict) or isinstance(params, list):
|
||||
params = urllib.urlencode(params)
|
||||
req = urllib2.Request(self.request.url, params)
|
||||
else:
|
||||
req = urllib2.Request(self.request.url)
|
||||
|
||||
for key, value in self.request.headers.iteritems():
|
||||
req.add_header(key, value)
|
||||
|
||||
if self.request.upload:
|
||||
req.add_header('Content-type', 'multipart/form-data; boundary=%s' % boundary)
|
||||
req.add_header('Content-length', len(upload))
|
||||
|
||||
if self.request.auth_username and self.request.auth_password:
|
||||
req.add_header('Authorization', 'Basic %s' % base64.encodestring(
|
||||
':'.join([self.request.auth_username, self.request.auth_password])).strip())
|
||||
|
||||
self.con = urllib2.urlopen(req, timeout=self.request.timeout)
|
||||
#self.con = urllib2.urlopen(req)
|
||||
self.response.headers = self._headers(self.con.info())
|
||||
|
||||
if self.request.download:
|
||||
self._download()
|
||||
else:
|
||||
self.response.body = self.con.read()
|
||||
|
||||
if self.request.cookies:
|
||||
self.cookies.save(self.request.cookies)
|
||||
|
||||
|
||||
def _download(self):
|
||||
fd = open(self.request.download, 'wb')
|
||||
if self.request.progress:
|
||||
self.progress = xbmcgui.DialogProgress()
|
||||
self.progress.create(u'Download')
|
||||
|
||||
bs = 1024 * 8
|
||||
size = -1
|
||||
read = 0
|
||||
name = None
|
||||
|
||||
if self.request.progress:
|
||||
if 'content-length' in self.response.headers:
|
||||
size = int(self.response.headers['content-length'])
|
||||
if 'content-disposition' in self.response.headers:
|
||||
r = RE['content-disposition'].search(self.response.headers['content-disposition'])
|
||||
if r:
|
||||
name = urllib.unquote(r.group(1))
|
||||
|
||||
while 1:
|
||||
buf = self.con.read(bs)
|
||||
if buf == '':
|
||||
break
|
||||
read += len(buf)
|
||||
fd.write(buf)
|
||||
|
||||
if self.request.progress:
|
||||
self.progress.update(*self._progress(read, size, name))
|
||||
|
||||
self.response.filename = self.request.download
|
||||
|
||||
|
||||
def _upload(self, upload, params):
|
||||
res = []
|
||||
boundary = mimetools.choose_boundary()
|
||||
part_boundary = '--' + boundary
|
||||
|
||||
if params:
|
||||
for name, value in params.iteritems():
|
||||
res.append([part_boundary, 'Content-Disposition: form-data; name="%s"' % name, '', value])
|
||||
|
||||
if isinstance(upload, dict):
|
||||
upload = [upload]
|
||||
|
||||
for obj in upload:
|
||||
name = obj.get('name')
|
||||
filename = obj.get('filename', 'default')
|
||||
content_type = obj.get('content-type')
|
||||
try:
|
||||
body = obj['body'].read()
|
||||
except AttributeError:
|
||||
body = obj['body']
|
||||
|
||||
if content_type:
|
||||
res.append([part_boundary,
|
||||
'Content-Disposition: file; name="%s"; filename="%s"' % (name, urllib.quote(filename)),
|
||||
'Content-Type: %s' % content_type, '', body])
|
||||
else:
|
||||
res.append([part_boundary,
|
||||
'Content-Disposition: file; name="%s"; filename="%s"' % (name, urllib.quote(filename)), '',
|
||||
body])
|
||||
|
||||
result = list(itertools.chain(*res))
|
||||
result.append('--' + boundary + '--')
|
||||
result.append('')
|
||||
return boundary, '\r\n'.join(result)
|
||||
|
||||
|
||||
def _headers(self, raw):
|
||||
headers = {}
|
||||
for line in raw.headers:
|
||||
pair = line.split(':', 1)
|
||||
if len(pair) == 2:
|
||||
tag = pair[0].lower().strip()
|
||||
value = pair[1].strip()
|
||||
if tag and value:
|
||||
headers[tag] = value
|
||||
return headers
|
||||
|
||||
|
||||
def _progress(self, read, size, name):
|
||||
res = []
|
||||
if size < 0:
|
||||
res.append(1)
|
||||
else:
|
||||
res.append(int(float(read) / (float(size) / 100.0)))
|
||||
if name:
|
||||
res.append(u'File: ' + name)
|
||||
if size != -1:
|
||||
res.append(u'Size: ' + self._human(size))
|
||||
res.append(u'Load: ' + self._human(read))
|
||||
return res
|
||||
|
||||
def _human(self, size):
|
||||
human = None
|
||||
for h, f in (('KB', 1024), ('MB', 1024 * 1024), ('GB', 1024 * 1024 * 1024), ('TB', 1024 * 1024 * 1024 * 1024)):
|
||||
if size / f > 0:
|
||||
human = h
|
||||
factor = f
|
||||
else:
|
||||
break
|
||||
if human is None:
|
||||
return (u'%10.1f %s' % (size, u'byte')).replace(u'.0', u'')
|
||||
else:
|
||||
return u'%10.2f %s' % (float(size) / float(factor), human)
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
def __init__(self, url, method='GET', headers=None, cookies=None, params=None, upload=None, download=None,
|
||||
progress=False, auth_username=None, auth_password=None, proxy_protocol='http', proxy_host=None,
|
||||
proxy_port=None, proxy_username=None, proxy_password='', timeout=20.0, redirect=True, gzip=False):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.headers = headers
|
||||
|
||||
self.cookies = cookies
|
||||
|
||||
self.params = params
|
||||
|
||||
self.upload = upload
|
||||
self.download = download
|
||||
self.progress = progress
|
||||
|
||||
self.auth_username = auth_username
|
||||
self.auth_password = auth_password
|
||||
|
||||
self.proxy_protocol = proxy_protocol
|
||||
self.proxy_host = proxy_host
|
||||
self.proxy_port = proxy_port
|
||||
self.proxy_username = proxy_username
|
||||
self.proxy_password = proxy_password
|
||||
|
||||
self.timeout = timeout
|
||||
|
||||
self.redirect = redirect
|
||||
|
||||
self.gzip = gzip
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (self.__class__.__name__, ','.join('%s=%r' % i for i in self.__dict__.iteritems()))
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.code = None
|
||||
self.headers = {}
|
||||
self.error = None
|
||||
self.body = None
|
||||
self.filename = None
|
||||
self.time = time.time()
|
||||
|
||||
def __repr__(self):
|
||||
args = ','.join('%s=%r' % i for i in self.__dict__.iteritems() if i[0] != 'body')
|
||||
if self.body:
|
||||
args += ',body=<data>'
|
||||
else:
|
||||
args += ',body=None'
|
||||
return '%s(%s)' % (self.__class__.__name__, args)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# __
|
||||
# /__) _ _ _ _ _/ _
|
||||
# / ( (- (/ (/ (- _) / _)
|
||||
# /
|
||||
|
||||
"""
|
||||
requests HTTP library
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Requests is an HTTP library, written in Python, for human beings. Basic GET
|
||||
usage:
|
||||
|
||||
>>> import requests
|
||||
>>> r = requests.get('https://www.python.org')
|
||||
>>> r.status_code
|
||||
200
|
||||
>>> 'Python is a programming language' in r.content
|
||||
True
|
||||
|
||||
... or POST:
|
||||
|
||||
>>> payload = dict(key1='value1', key2='value2')
|
||||
>>> r = requests.post('http://httpbin.org/post', data=payload)
|
||||
>>> print(r.text)
|
||||
{
|
||||
...
|
||||
"form": {
|
||||
"key2": "value2",
|
||||
"key1": "value1"
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
The other HTTP methods are supported - see `requests.api`. Full documentation
|
||||
is at <http://python-requests.org>.
|
||||
|
||||
:copyright: (c) 2014 by Kenneth Reitz.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'requests'
|
||||
__version__ = '2.5.0'
|
||||
__build__ = 0x020500
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'Apache 2.0'
|
||||
__copyright__ = 'Copyright 2014 Kenneth Reitz'
|
||||
|
||||
# Attempt to enable urllib3's SNI support, if possible
|
||||
try:
|
||||
from .packages.urllib3.contrib import pyopenssl
|
||||
pyopenssl.inject_into_urllib3()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from . import utils
|
||||
from .models import Request, Response, PreparedRequest
|
||||
from .api import request, get, head, post, patch, put, delete, options
|
||||
from .sessions import session, Session
|
||||
from .status_codes import codes
|
||||
from .exceptions import (
|
||||
RequestException, Timeout, URLRequired,
|
||||
TooManyRedirects, HTTPError, ConnectionError
|
||||
)
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
@@ -0,0 +1,437 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
requests.adapters
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains the transport adapters that Requests uses to define
|
||||
and maintain connections.
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
from .models import Response
|
||||
from .packages.urllib3 import Retry
|
||||
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from .packages.urllib3.response import HTTPResponse
|
||||
from .packages.urllib3.util import Timeout as TimeoutSauce
|
||||
from .compat import urlparse, basestring
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
prepend_scheme_if_needed, get_auth_from_url, urldefragauth)
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .packages.urllib3.exceptions import ConnectTimeoutError
|
||||
from .packages.urllib3.exceptions import HTTPError as _HTTPError
|
||||
from .packages.urllib3.exceptions import MaxRetryError
|
||||
from .packages.urllib3.exceptions import ProxyError as _ProxyError
|
||||
from .packages.urllib3.exceptions import ProtocolError
|
||||
from .packages.urllib3.exceptions import ReadTimeoutError
|
||||
from .packages.urllib3.exceptions import SSLError as _SSLError
|
||||
from .packages.urllib3.exceptions import ResponseError
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
|
||||
ProxyError, RetryError)
|
||||
from .auth import _basic_auth_str
|
||||
|
||||
DEFAULT_POOLBLOCK = False
|
||||
DEFAULT_POOLSIZE = 10
|
||||
DEFAULT_RETRIES = 0
|
||||
|
||||
|
||||
class BaseAdapter(object):
|
||||
"""The Base Transport Adapter"""
|
||||
|
||||
def __init__(self):
|
||||
super(BaseAdapter, self).__init__()
|
||||
|
||||
def send(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTTPAdapter(BaseAdapter):
|
||||
"""The built-in HTTP Adapter for urllib3.
|
||||
|
||||
Provides a general-case interface for Requests sessions to contact HTTP and
|
||||
HTTPS urls by implementing the Transport Adapter interface. This class will
|
||||
usually be created by the :class:`Session <Session>` class under the
|
||||
covers.
|
||||
|
||||
:param pool_connections: The number of urllib3 connection pools to cache.
|
||||
:param pool_maxsize: The maximum number of connections to save in the pool.
|
||||
:param int max_retries: The maximum number of retries each connection
|
||||
should attempt. Note, this applies only to failed DNS lookups, socket
|
||||
connections and connection timeouts, never to requests where data has
|
||||
made it to the server. By default, Requests does not retry failed
|
||||
connections. If you need granular control over the conditions under
|
||||
which we retry a request, import urllib3's ``Retry`` class and pass
|
||||
that instead.
|
||||
:param pool_block: Whether the connection pool should block for connections.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> s = requests.Session()
|
||||
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
>>> s.mount('http://', a)
|
||||
"""
|
||||
__attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize',
|
||||
'_pool_block']
|
||||
|
||||
def __init__(self, pool_connections=DEFAULT_POOLSIZE,
|
||||
pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
|
||||
pool_block=DEFAULT_POOLBLOCK):
|
||||
if max_retries == DEFAULT_RETRIES:
|
||||
self.max_retries = Retry(0, read=False)
|
||||
else:
|
||||
self.max_retries = Retry.from_int(max_retries)
|
||||
self.config = {}
|
||||
self.proxy_manager = {}
|
||||
|
||||
super(HTTPAdapter, self).__init__()
|
||||
|
||||
self._pool_connections = pool_connections
|
||||
self._pool_maxsize = pool_maxsize
|
||||
self._pool_block = pool_block
|
||||
|
||||
self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
|
||||
|
||||
def __getstate__(self):
|
||||
return dict((attr, getattr(self, attr, None)) for attr in
|
||||
self.__attrs__)
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Can't handle by adding 'proxy_manager' to self.__attrs__ because
|
||||
# because self.poolmanager uses a lambda function, which isn't pickleable.
|
||||
self.proxy_manager = {}
|
||||
self.config = {}
|
||||
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
self.init_poolmanager(self._pool_connections, self._pool_maxsize,
|
||||
block=self._pool_block)
|
||||
|
||||
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
|
||||
"""Initializes a urllib3 PoolManager.
|
||||
|
||||
This method should not be called from user code, and is only
|
||||
exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param connections: The number of urllib3 connection pools to cache.
|
||||
:param maxsize: The maximum number of connections to save in the pool.
|
||||
:param block: Block when no free connections are available.
|
||||
:param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.
|
||||
"""
|
||||
# save these values for pickling
|
||||
self._pool_connections = connections
|
||||
self._pool_maxsize = maxsize
|
||||
self._pool_block = block
|
||||
|
||||
self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
|
||||
block=block, strict=True, **pool_kwargs)
|
||||
|
||||
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
||||
"""Return urllib3 ProxyManager for the given proxy.
|
||||
|
||||
This method should not be called from user code, and is only
|
||||
exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param proxy: The proxy to return a urllib3 ProxyManager for.
|
||||
:param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager.
|
||||
:returns: ProxyManager
|
||||
"""
|
||||
if not proxy in self.proxy_manager:
|
||||
proxy_headers = self.proxy_headers(proxy)
|
||||
self.proxy_manager[proxy] = proxy_from_url(
|
||||
proxy,
|
||||
proxy_headers=proxy_headers,
|
||||
num_pools=self._pool_connections,
|
||||
maxsize=self._pool_maxsize,
|
||||
block=self._pool_block,
|
||||
**proxy_kwargs)
|
||||
|
||||
return self.proxy_manager[proxy]
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
"""Verify a SSL certificate. This method should not be called from user
|
||||
code, and is only exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param conn: The urllib3 connection object associated with the cert.
|
||||
:param url: The requested URL.
|
||||
:param verify: Whether we should actually verify the certificate.
|
||||
:param cert: The SSL certificate to verify.
|
||||
"""
|
||||
if url.lower().startswith('https') and verify:
|
||||
|
||||
cert_loc = None
|
||||
|
||||
# Allow self-specified cert location.
|
||||
if verify is not True:
|
||||
cert_loc = verify
|
||||
|
||||
if not cert_loc:
|
||||
cert_loc = DEFAULT_CA_BUNDLE_PATH
|
||||
|
||||
if not cert_loc:
|
||||
raise Exception("Could not find a suitable SSL CA certificate bundle.")
|
||||
|
||||
conn.cert_reqs = 'CERT_REQUIRED'
|
||||
conn.ca_certs = cert_loc
|
||||
else:
|
||||
conn.cert_reqs = 'CERT_NONE'
|
||||
conn.ca_certs = None
|
||||
|
||||
if cert:
|
||||
if not isinstance(cert, basestring):
|
||||
conn.cert_file = cert[0]
|
||||
conn.key_file = cert[1]
|
||||
else:
|
||||
conn.cert_file = cert
|
||||
|
||||
def build_response(self, req, resp):
|
||||
"""Builds a :class:`Response <requests.Response>` object from a urllib3
|
||||
response. This should not be called from user code, and is only exposed
|
||||
for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`
|
||||
|
||||
:param req: The :class:`PreparedRequest <PreparedRequest>` used to generate the response.
|
||||
:param resp: The urllib3 response object.
|
||||
"""
|
||||
response = Response()
|
||||
|
||||
# Fallback to None if there's no status_code, for whatever reason.
|
||||
response.status_code = getattr(resp, 'status', None)
|
||||
|
||||
# Make headers case-insensitive.
|
||||
response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
|
||||
|
||||
# Set encoding.
|
||||
response.encoding = get_encoding_from_headers(response.headers)
|
||||
response.raw = resp
|
||||
response.reason = response.raw.reason
|
||||
|
||||
if isinstance(req.url, bytes):
|
||||
response.url = req.url.decode('utf-8')
|
||||
else:
|
||||
response.url = req.url
|
||||
|
||||
# Add new cookies from the server.
|
||||
extract_cookies_to_jar(response.cookies, req, resp)
|
||||
|
||||
# Give the Response some context.
|
||||
response.request = req
|
||||
response.connection = self
|
||||
|
||||
return response
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
"""Returns a urllib3 connection for the given URL. This should not be
|
||||
called from user code, and is only exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param url: The URL to connect to.
|
||||
:param proxies: (optional) A Requests-style dictionary of proxies used on this request.
|
||||
"""
|
||||
proxies = proxies or {}
|
||||
proxy = proxies.get(urlparse(url.lower()).scheme)
|
||||
|
||||
if proxy:
|
||||
proxy = prepend_scheme_if_needed(proxy, 'http')
|
||||
proxy_manager = self.proxy_manager_for(proxy)
|
||||
conn = proxy_manager.connection_from_url(url)
|
||||
else:
|
||||
# Only scheme should be lower case
|
||||
parsed = urlparse(url)
|
||||
url = parsed.geturl()
|
||||
conn = self.poolmanager.connection_from_url(url)
|
||||
|
||||
return conn
|
||||
|
||||
def close(self):
|
||||
"""Disposes of any internal state.
|
||||
|
||||
Currently, this just closes the PoolManager, which closes pooled
|
||||
connections.
|
||||
"""
|
||||
self.poolmanager.clear()
|
||||
|
||||
def request_url(self, request, proxies):
|
||||
"""Obtain the url to use when making the final request.
|
||||
|
||||
If the message is being sent through a HTTP proxy, the full URL has to
|
||||
be used. Otherwise, we should only use the path portion of the URL.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param proxies: A dictionary of schemes to proxy URLs.
|
||||
"""
|
||||
proxies = proxies or {}
|
||||
scheme = urlparse(request.url).scheme
|
||||
proxy = proxies.get(scheme)
|
||||
|
||||
if proxy and scheme != 'https':
|
||||
url = urldefragauth(request.url)
|
||||
else:
|
||||
url = request.path_url
|
||||
|
||||
return url
|
||||
|
||||
def add_headers(self, request, **kwargs):
|
||||
"""Add any headers needed by the connection. As of v2.0 this does
|
||||
nothing by default, but is left for overriding by users that subclass
|
||||
the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` to add headers to.
|
||||
:param kwargs: The keyword arguments from the call to send().
|
||||
"""
|
||||
pass
|
||||
|
||||
def proxy_headers(self, proxy):
|
||||
"""Returns a dictionary of the headers to add to any request sent
|
||||
through a proxy. This works with urllib3 magic to ensure that they are
|
||||
correctly sent to the proxy, rather than in a tunnelled request if
|
||||
CONNECT is being used.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param proxies: The url of the proxy being used for this request.
|
||||
:param kwargs: Optional additional keyword arguments.
|
||||
"""
|
||||
headers = {}
|
||||
username, password = get_auth_from_url(proxy)
|
||||
|
||||
if username and password:
|
||||
headers['Proxy-Authorization'] = _basic_auth_str(username,
|
||||
password)
|
||||
|
||||
return headers
|
||||
|
||||
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
|
||||
"""Sends PreparedRequest object. Returns Response object.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param stream: (optional) Whether to stream the request content.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a (`connect timeout, read
|
||||
timeout <user/advanced.html#timeouts>`_) tuple.
|
||||
:type timeout: float or tuple
|
||||
:param verify: (optional) Whether to verify SSL certificates.
|
||||
:param cert: (optional) Any user-provided SSL certificate to be trusted.
|
||||
:param proxies: (optional) The proxies dictionary to apply to the request.
|
||||
"""
|
||||
|
||||
conn = self.get_connection(request.url, proxies)
|
||||
|
||||
self.cert_verify(conn, request.url, verify, cert)
|
||||
url = self.request_url(request, proxies)
|
||||
self.add_headers(request)
|
||||
|
||||
chunked = not (request.body is None or 'Content-Length' in request.headers)
|
||||
|
||||
if isinstance(timeout, tuple):
|
||||
try:
|
||||
connect, read = timeout
|
||||
timeout = TimeoutSauce(connect=connect, read=read)
|
||||
except ValueError as e:
|
||||
# this may raise a string formatting error.
|
||||
err = ("Invalid timeout {0}. Pass a (connect, read) "
|
||||
"timeout tuple, or a single float to set "
|
||||
"both timeouts to the same value".format(timeout))
|
||||
raise ValueError(err)
|
||||
else:
|
||||
timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
|
||||
try:
|
||||
if not chunked:
|
||||
resp = conn.urlopen(
|
||||
method=request.method,
|
||||
url=url,
|
||||
body=request.body,
|
||||
headers=request.headers,
|
||||
redirect=False,
|
||||
assert_same_host=False,
|
||||
preload_content=False,
|
||||
decode_content=False,
|
||||
retries=self.max_retries,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# Send the request.
|
||||
else:
|
||||
if hasattr(conn, 'proxy_pool'):
|
||||
conn = conn.proxy_pool
|
||||
|
||||
low_conn = conn._get_conn(timeout=timeout)
|
||||
|
||||
try:
|
||||
low_conn.putrequest(request.method,
|
||||
url,
|
||||
skip_accept_encoding=True)
|
||||
|
||||
for header, value in request.headers.items():
|
||||
low_conn.putheader(header, value)
|
||||
|
||||
low_conn.endheaders()
|
||||
|
||||
for i in request.body:
|
||||
low_conn.send(hex(len(i))[2:].encode('utf-8'))
|
||||
low_conn.send(b'\r\n')
|
||||
low_conn.send(i)
|
||||
low_conn.send(b'\r\n')
|
||||
low_conn.send(b'0\r\n\r\n')
|
||||
|
||||
r = low_conn.getresponse()
|
||||
resp = HTTPResponse.from_httplib(
|
||||
r,
|
||||
pool=conn,
|
||||
connection=low_conn,
|
||||
preload_content=False,
|
||||
decode_content=False
|
||||
)
|
||||
except:
|
||||
# If we hit any problems here, clean up the connection.
|
||||
# Then, reraise so that we can handle the actual exception.
|
||||
low_conn.close()
|
||||
raise
|
||||
else:
|
||||
# All is well, return the connection to the pool.
|
||||
conn._put_conn(low_conn)
|
||||
|
||||
except (ProtocolError, socket.error) as err:
|
||||
raise ConnectionError(err, request=request)
|
||||
|
||||
except MaxRetryError as e:
|
||||
if isinstance(e.reason, ConnectTimeoutError):
|
||||
raise ConnectTimeout(e, request=request)
|
||||
|
||||
if isinstance(e.reason, ResponseError):
|
||||
raise RetryError(e, request=request)
|
||||
|
||||
raise ConnectionError(e, request=request)
|
||||
|
||||
except _ProxyError as e:
|
||||
raise ProxyError(e)
|
||||
|
||||
except (_SSLError, _HTTPError) as e:
|
||||
if isinstance(e, _SSLError):
|
||||
raise SSLError(e, request=request)
|
||||
elif isinstance(e, ReadTimeoutError):
|
||||
raise ReadTimeout(e, request=request)
|
||||
else:
|
||||
raise
|
||||
|
||||
return self.build_response(request, resp)
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
requests.api
|
||||
~~~~~~~~~~~~
|
||||
|
||||
This module implements the Requests API.
|
||||
|
||||
:copyright: (c) 2012 by Kenneth Reitz.
|
||||
:license: Apache2, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
from . import sessions
|
||||
|
||||
|
||||
def request(method, url, **kwargs):
|
||||
"""Constructs and sends a :class:`Request <Request>`.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
:param method: method for the new :class:`Request` object.
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) How long to wait for the server to send data
|
||||
before giving up, as a float, or a (`connect timeout, read timeout
|
||||
<user/advanced.html#timeouts>`_) tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
||||
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> req = requests.request('GET', 'http://httpbin.org/get')
|
||||
<Response [200]>
|
||||
"""
|
||||
|
||||
session = sessions.Session()
|
||||
response = session.request(method=method, url=url, **kwargs)
|
||||
# By explicitly closing the session, we avoid leaving sockets open which
|
||||
# can trigger a ResourceWarning in some cases, and look like a memory leak
|
||||
# in others.
|
||||
session.close()
|
||||
return response
|
||||
|
||||
|
||||
def get(url, **kwargs):
|
||||
"""Sends a GET request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
return request('get', url, **kwargs)
|
||||
|
||||
|
||||
def options(url, **kwargs):
|
||||
"""Sends a OPTIONS request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
return request('options', url, **kwargs)
|
||||
|
||||
|
||||
def head(url, **kwargs):
|
||||
"""Sends a HEAD request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', False)
|
||||
return request('head', url, **kwargs)
|
||||
|
||||
|
||||
def post(url, data=None, json=None, **kwargs):
|
||||
"""Sends a POST request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return request('post', url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
"""Sends a PUT request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return request('put', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **kwargs):
|
||||
"""Sends a PATCH request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return request('patch', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **kwargs):
|
||||
"""Sends a DELETE request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return request('delete', url, **kwargs)
|
||||
@@ -0,0 +1,208 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
requests.auth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This module contains the authentication handlers for Requests.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from .compat import urlparse, str
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .utils import parse_dict_header, to_native_string
|
||||
from .status_codes import codes
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
|
||||
|
||||
|
||||
def _basic_auth_str(username, password):
|
||||
"""Returns a Basic Auth string."""
|
||||
|
||||
authstr = 'Basic ' + to_native_string(
|
||||
b64encode(('%s:%s' % (username, password)).encode('latin1')).strip()
|
||||
)
|
||||
|
||||
return authstr
|
||||
|
||||
|
||||
class AuthBase(object):
|
||||
"""Base class that all auth implementations derive from"""
|
||||
|
||||
def __call__(self, r):
|
||||
raise NotImplementedError('Auth hooks must be callable.')
|
||||
|
||||
|
||||
class HTTPBasicAuth(AuthBase):
|
||||
"""Attaches HTTP Basic Authentication to the given Request object."""
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def __call__(self, r):
|
||||
r.headers['Authorization'] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
|
||||
class HTTPProxyAuth(HTTPBasicAuth):
|
||||
"""Attaches HTTP Proxy Authentication to a given Request object."""
|
||||
def __call__(self, r):
|
||||
r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
|
||||
class HTTPDigestAuth(AuthBase):
|
||||
"""Attaches HTTP Digest Authentication to the given Request object."""
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.last_nonce = ''
|
||||
self.nonce_count = 0
|
||||
self.chal = {}
|
||||
self.pos = None
|
||||
|
||||
def build_digest_header(self, method, url):
|
||||
|
||||
realm = self.chal['realm']
|
||||
nonce = self.chal['nonce']
|
||||
qop = self.chal.get('qop')
|
||||
algorithm = self.chal.get('algorithm')
|
||||
opaque = self.chal.get('opaque')
|
||||
|
||||
if algorithm is None:
|
||||
_algorithm = 'MD5'
|
||||
else:
|
||||
_algorithm = algorithm.upper()
|
||||
# lambdas assume digest modules are imported at the top level
|
||||
if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
|
||||
def md5_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode('utf-8')
|
||||
return hashlib.md5(x).hexdigest()
|
||||
hash_utf8 = md5_utf8
|
||||
elif _algorithm == 'SHA':
|
||||
def sha_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode('utf-8')
|
||||
return hashlib.sha1(x).hexdigest()
|
||||
hash_utf8 = sha_utf8
|
||||
|
||||
KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
|
||||
|
||||
if hash_utf8 is None:
|
||||
return None
|
||||
|
||||
# XXX not implemented yet
|
||||
entdig = None
|
||||
p_parsed = urlparse(url)
|
||||
path = p_parsed.path
|
||||
if p_parsed.query:
|
||||
path += '?' + p_parsed.query
|
||||
|
||||
A1 = '%s:%s:%s' % (self.username, realm, self.password)
|
||||
A2 = '%s:%s' % (method, path)
|
||||
|
||||
HA1 = hash_utf8(A1)
|
||||
HA2 = hash_utf8(A2)
|
||||
|
||||
if nonce == self.last_nonce:
|
||||
self.nonce_count += 1
|
||||
else:
|
||||
self.nonce_count = 1
|
||||
ncvalue = '%08x' % self.nonce_count
|
||||
s = str(self.nonce_count).encode('utf-8')
|
||||
s += nonce.encode('utf-8')
|
||||
s += time.ctime().encode('utf-8')
|
||||
s += os.urandom(8)
|
||||
|
||||
cnonce = (hashlib.sha1(s).hexdigest()[:16])
|
||||
noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, HA2)
|
||||
if _algorithm == 'MD5-SESS':
|
||||
HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
|
||||
|
||||
if qop is None:
|
||||
respdig = KD(HA1, "%s:%s" % (nonce, HA2))
|
||||
elif qop == 'auth' or 'auth' in qop.split(','):
|
||||
respdig = KD(HA1, noncebit)
|
||||
else:
|
||||
# XXX handle auth-int.
|
||||
return None
|
||||
|
||||
self.last_nonce = nonce
|
||||
|
||||
# XXX should the partial digests be encoded too?
|
||||
base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
|
||||
'response="%s"' % (self.username, realm, nonce, path, respdig)
|
||||
if opaque:
|
||||
base += ', opaque="%s"' % opaque
|
||||
if algorithm:
|
||||
base += ', algorithm="%s"' % algorithm
|
||||
if entdig:
|
||||
base += ', digest="%s"' % entdig
|
||||
if qop:
|
||||
base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
|
||||
|
||||
return 'Digest %s' % (base)
|
||||
|
||||
def handle_redirect(self, r, **kwargs):
|
||||
"""Reset num_401_calls counter on redirects."""
|
||||
if r.is_redirect:
|
||||
setattr(self, 'num_401_calls', 1)
|
||||
|
||||
def handle_401(self, r, **kwargs):
|
||||
"""Takes the given response and tries digest-auth, if needed."""
|
||||
|
||||
if self.pos is not None:
|
||||
# Rewind the file position indicator of the body to where
|
||||
# it was to resend the request.
|
||||
r.request.body.seek(self.pos)
|
||||
num_401_calls = getattr(self, 'num_401_calls', 1)
|
||||
s_auth = r.headers.get('www-authenticate', '')
|
||||
|
||||
if 'digest' in s_auth.lower() and num_401_calls < 2:
|
||||
|
||||
setattr(self, 'num_401_calls', num_401_calls + 1)
|
||||
pat = re.compile(r'digest ', flags=re.IGNORECASE)
|
||||
self.chal = parse_dict_header(pat.sub('', s_auth, count=1))
|
||||
|
||||
# Consume content and release the original connection
|
||||
# to allow our new request to reuse the same one.
|
||||
r.content
|
||||
r.raw.release_conn()
|
||||
prep = r.request.copy()
|
||||
extract_cookies_to_jar(prep._cookies, r.request, r.raw)
|
||||
prep.prepare_cookies(prep._cookies)
|
||||
|
||||
prep.headers['Authorization'] = self.build_digest_header(
|
||||
prep.method, prep.url)
|
||||
_r = r.connection.send(prep, **kwargs)
|
||||
_r.history.append(r)
|
||||
_r.request = prep
|
||||
|
||||
return _r
|
||||
|
||||
setattr(self, 'num_401_calls', num_401_calls + 1)
|
||||
return r
|
||||
|
||||
def __call__(self, r):
|
||||
# If we have a saved nonce, skip the 401
|
||||
if self.last_nonce:
|
||||
r.headers['Authorization'] = self.build_digest_header(r.method, r.url)
|
||||
try:
|
||||
self.pos = r.body.tell()
|
||||
except AttributeError:
|
||||
# In the case of HTTPDigestAuth being reused and the body of
|
||||
# the previous request was a file-like object, pos has the
|
||||
# file position of the previous body. Ensure it's set to
|
||||
# None.
|
||||
self.pos = None
|
||||
r.register_hook('response', self.handle_401)
|
||||
r.register_hook('response', self.handle_redirect)
|
||||
return r
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
certs.py
|
||||
~~~~~~~~
|
||||
|
||||
This module returns the preferred default CA certificate bundle.
|
||||
|
||||
If you are packaging Requests, e.g., for a Linux distribution or a managed
|
||||
environment, you can change the definition of where() to return a separately
|
||||
packaged CA bundle.
|
||||
"""
|
||||
import os.path
|
||||
|
||||
try:
|
||||
from certifi import where
|
||||
except ImportError:
|
||||
def where():
|
||||
"""Return the preferred certificate bundle."""
|
||||
# vendored bundle inside Requests
|
||||
return os.path.join(os.path.dirname(__file__), 'cacert.pem')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(where())
|
||||