why?
This commit is contained in:
DiMartinoX committed 2015-01-09 14:11:21 +03:00
commit 6fef9834fe
396 files changed
+49459

No files matched your search

View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+191
View File
@@ -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)
'''
Binary file not shown.
+91
View File
@@ -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
Binary file not shown.
+146
View File
@@ -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
Binary file not shown.
+248
View File
@@ -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)
'''
Binary file not shown.
+310
View File
@@ -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)
'''
Binary file not shown.
+110
View File
@@ -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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

+49
View File
@@ -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>
+50
View File
@@ -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>
View File
Whitespace-only changes.
Binary file not shown.
+774
View File
@@ -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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+164
View File
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
__version__ = '0.4.0'
Binary file not shown.
Binary file not shown.
+270
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+135
View File
@@ -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()
Binary file not shown.
Binary file not shown.
+76
View File
@@ -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))
Binary file not shown.
Binary file not shown.
+57
View File
@@ -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 = {
'&#151;': '-'
}
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
Binary file not shown.
Binary file not shown.
+215
View File
@@ -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)
Binary file not shown.
+40
View File
@@ -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")
Binary file not shown.
+96
View File
@@ -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'))
Binary file not shown.
+382
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+175
View File
@@ -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('&nbsp;', '')
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
Binary file not shown.
@@ -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/'
Binary file not shown.
+288
View File
@@ -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('\&amp\;|\&', ' 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
Binary file not shown.
+565
View File
@@ -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">&laquo;(.+?)&raquo;</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]+)&times;([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
Binary file not shown.
+307
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+77
View File
@@ -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())
Binary file not shown.
Binary file not shown.
+437
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+131
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+208
View File
@@ -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
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
+25
View File
@@ -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())
Binary file not shown.
Loaded 100 of 396 files, more files were not shown because too many files have changed in this diff. Show more