mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
4c141ab55a
@ -1,134 +1,129 @@
|
|||||||
#!/usr/bin/env python
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from datetime import datetime, timedelta
|
||||||
from datetime import datetime, timedelta
|
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
|
||||||
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
|
from calibre.utils.magick import Image, PixelWand
|
||||||
from calibre.utils.magick import Image, PixelWand
|
from urllib2 import Request, urlopen, URLError
|
||||||
from urllib2 import Request, urlopen, URLError
|
|
||||||
|
class Estadao(BasicNewsRecipe):
|
||||||
class Estadao(BasicNewsRecipe):
|
THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
|
||||||
THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/
|
LANGUAGE = 'pt_br'
|
||||||
LANGUAGE = 'pt_br'
|
language = 'pt'
|
||||||
language = 'pt'
|
LANGHTM = 'pt-br'
|
||||||
LANGHTM = 'pt-br'
|
ENCODING = 'utf'
|
||||||
ENCODING = 'utf'
|
ENCHTM = 'utf-8'
|
||||||
ENCHTM = 'utf-8'
|
directionhtm = 'ltr'
|
||||||
directionhtm = 'ltr'
|
requires_version = (0,7,47)
|
||||||
requires_version = (0,8,47)
|
news = True
|
||||||
news = True
|
|
||||||
publication_type = 'newsportal'
|
title = u'Estad\xe3o'
|
||||||
|
__author__ = 'Euler Alves'
|
||||||
title = u'Estadao'
|
description = u'Brazilian news from Estad\xe3o'
|
||||||
__author__ = 'Euler Alves'
|
publisher = u'Estad\xe3o'
|
||||||
description = u'Brazilian news from Estad\xe3o'
|
category = 'news, rss'
|
||||||
publisher = u'Estad\xe3o'
|
|
||||||
category = 'news, rss'
|
oldest_article = 4
|
||||||
|
max_articles_per_feed = 100
|
||||||
oldest_article = 4
|
summary_length = 1000
|
||||||
max_articles_per_feed = 100
|
|
||||||
summary_length = 1000
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
remove_javascript = True
|
use_embedded_content = False
|
||||||
no_stylesheets = True
|
remove_empty_feeds = True
|
||||||
use_embedded_content = False
|
timefmt = ' [%d %b %Y (%a)]'
|
||||||
remove_empty_feeds = True
|
|
||||||
timefmt = ' [%d %b %Y (%a)]'
|
hoje = datetime.now()-timedelta(days=2)
|
||||||
|
pubdate = hoje.strftime('%a, %d %b')
|
||||||
html2lrf_options = [
|
if hoje.hour<10:
|
||||||
'--comment', description
|
hoje = hoje-timedelta(days=1)
|
||||||
,'--category', category
|
CAPA = 'http://www.estadao.com.br/estadaodehoje/'+hoje.strftime('%Y%m%d')+'/img/capadodia.jpg'
|
||||||
,'--publisher', publisher
|
SCREENSHOT = 'http://estadao.com.br/'
|
||||||
]
|
cover_margins = (0,0,'white')
|
||||||
|
masthead_url = 'http://www.estadao.com.br/estadao/novo/img/logo.png'
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'class':['bb-md-noticia','corpo']})]
|
||||||
hoje = datetime.now()-timedelta(days=2)
|
remove_tags = [
|
||||||
pubdate = hoje.strftime('%a, %d %b')
|
dict(name='div',
|
||||||
if hoje.hour<10:
|
attrs={'id':[
|
||||||
hoje = hoje-timedelta(days=1)
|
'bb-md-noticia-tabs'
|
||||||
CAPA = 'http://www.estadao.com.br/estadaodehoje/'+hoje.strftime('%Y%m%d')+'/img/capadodia.jpg'
|
]})
|
||||||
SCREENSHOT = 'http://estadao.com.br/'
|
,dict(name='div',
|
||||||
cover_margins = (0,0,'white')
|
attrs={'class':[
|
||||||
masthead_url = 'http://www.estadao.com.br/estadao/novo/img/logo.png'
|
'tags'
|
||||||
|
,'discussion'
|
||||||
keep_only_tags = [dict(name='div', attrs={'class':['bb-md-noticia','corpo']})]
|
,'bb-gg adsense_container'
|
||||||
remove_tags = [
|
]})
|
||||||
dict(name='div',
|
|
||||||
attrs={'id':[
|
,dict(name='a')
|
||||||
'bb-md-noticia-tabs'
|
,dict(name='iframe')
|
||||||
]})
|
,dict(name='link')
|
||||||
,dict(name='div',
|
,dict(name='script')
|
||||||
attrs={'class':[
|
]
|
||||||
'tags'
|
|
||||||
,'discussion'
|
|
||||||
,'bb-gg adsense_container'
|
feeds = [
|
||||||
]})
|
(u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml')
|
||||||
|
,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml')
|
||||||
,dict(name='a')
|
,(u'Brasil', u'http://www.estadao.com.br/rss/brasil.xml')
|
||||||
,dict(name='iframe')
|
,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml')
|
||||||
,dict(name='link')
|
,(u'Cinema', u'http://blogs.estadao.com.br/cinema/feed/')
|
||||||
,dict(name='script')
|
,(u'Planeta', u'http://www.estadao.com.br/rss/planeta.xml')
|
||||||
]
|
,(u'Ci\xEAncia', u'http://www.estadao.com.br/rss/ciencia.xml')
|
||||||
|
,(u'Sa\xFAde', u'http://www.estadao.com.br/rss/saude.xml')
|
||||||
feeds = [
|
,(u'Pol\xEDtica', u'http://www.estadao.com.br/rss/politica.xml')
|
||||||
(u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml')
|
]
|
||||||
,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml')
|
|
||||||
,(u'Brasil', u'http://www.estadao.com.br/rss/brasil.xml')
|
conversion_options = {
|
||||||
,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml')
|
'title' : title
|
||||||
,(u'Cinema', u'http://blogs.estadao.com.br/cinema/feed/')
|
,'comments' : description
|
||||||
,(u'Planeta', u'http://www.estadao.com.br/rss/planeta.xml')
|
,'publisher' : publisher
|
||||||
,(u'Ci\xEAncia', u'http://www.estadao.com.br/rss/ciencia.xml')
|
,'tags' : category
|
||||||
,(u'Sa\xFAde', u'http://www.estadao.com.br/rss/saude.xml')
|
,'language' : LANGUAGE
|
||||||
,(u'Pol\xEDtica', u'http://www.estadao.com.br/rss/politica.xml')
|
,'linearize_tables': True
|
||||||
]
|
}
|
||||||
|
|
||||||
conversion_options = {
|
def preprocess_html(self, soup):
|
||||||
'title' : title
|
for item in soup.findAll(style=True):
|
||||||
,'comments' : description
|
del item['style']
|
||||||
,'publisher' : publisher
|
if not soup.find(attrs={'http-equiv':'Content-Language'}):
|
||||||
,'tags' : category
|
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
|
||||||
,'language' : LANGUAGE
|
soup.head.insert(0,meta0)
|
||||||
,'linearize_tables': True
|
if not soup.find(attrs={'http-equiv':'Content-Type'}):
|
||||||
}
|
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
|
||||||
|
soup.head.insert(0,meta1)
|
||||||
def preprocess_html(self, soup):
|
return soup
|
||||||
for item in soup.findAll(style=True):
|
|
||||||
del item['style']
|
def postprocess_html(self, soup, first):
|
||||||
if not soup.find(attrs={'http-equiv':'Content-Language'}):
|
#process all the images. assumes that the new html has the correct path
|
||||||
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
|
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
|
||||||
soup.head.insert(0,meta0)
|
iurl = tag['src']
|
||||||
if not soup.find(attrs={'http-equiv':'Content-Type'}):
|
img = Image()
|
||||||
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
|
img.open(iurl)
|
||||||
soup.head.insert(0,meta1)
|
width, height = img.size
|
||||||
return soup
|
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
|
||||||
|
if img < 0:
|
||||||
def postprocess_html(self, soup, first):
|
raise RuntimeError('Out of memory')
|
||||||
#process all the images. assumes that the new html has the correct path
|
pw = PixelWand()
|
||||||
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
|
if( width > height and width > 590) :
|
||||||
iurl = tag['src']
|
print 'Rotate image'
|
||||||
img = Image()
|
img.rotate(pw, -90)
|
||||||
img.open(iurl)
|
img.save(iurl)
|
||||||
width, height = img.size
|
return soup
|
||||||
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
|
|
||||||
pw = PixelWand()
|
def get_cover_url(self):
|
||||||
if( width > height and width > 590) :
|
if self.THUMBALIZR_API:
|
||||||
print 'Rotate image'
|
cover_url = self.CAPA
|
||||||
img.rotate(pw, -90)
|
pedido = Request(self.CAPA)
|
||||||
img.save(iurl)
|
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
|
||||||
return soup
|
pedido.add_header('Accept-Charset',self.ENCHTM)
|
||||||
|
pedido.add_header('Referer',self.SCREENSHOT)
|
||||||
def get_cover_url(self):
|
try:
|
||||||
cover_url = self.CAPA
|
resposta = urlopen(pedido)
|
||||||
pedido = Request(self.CAPA)
|
soup = BeautifulSoup(resposta)
|
||||||
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
|
cover_item = soup.find('body')
|
||||||
pedido.add_header('Accept-Charset',self.ENCHTM)
|
if cover_item:
|
||||||
pedido.add_header('Referer',self.SCREENSHOT)
|
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
||||||
try:
|
return cover_url
|
||||||
resposta = urlopen(pedido)
|
except URLError:
|
||||||
soup = BeautifulSoup(resposta)
|
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
||||||
cover_item = soup.find('body')
|
return cover_url
|
||||||
if cover_item:
|
|
||||||
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
|
||||||
return cover_url
|
|
||||||
except URLError:
|
|
||||||
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
|
||||||
return cover_url
|
|
||||||
|
@ -1,149 +1,151 @@
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
|
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
|
||||||
from calibre.utils.magick import Image, PixelWand
|
from calibre.utils.magick import Image, PixelWand
|
||||||
from urllib2 import Request, urlopen, URLError
|
from urllib2 import Request, urlopen, URLError
|
||||||
|
|
||||||
class FolhaOnline(BasicNewsRecipe):
|
class FolhaOnline(BasicNewsRecipe):
|
||||||
THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/
|
THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
|
||||||
LANGUAGE = 'pt_br'
|
LANGUAGE = 'pt_br'
|
||||||
language = 'pt'
|
language = 'pt'
|
||||||
LANGHTM = 'pt-br'
|
LANGHTM = 'pt-br'
|
||||||
ENCODING = 'cp1252'
|
ENCODING = 'cp1252'
|
||||||
ENCHTM = 'iso-8859-1'
|
ENCHTM = 'iso-8859-1'
|
||||||
directionhtm = 'ltr'
|
directionhtm = 'ltr'
|
||||||
requires_version = (0,8,47)
|
requires_version = (0,7,47)
|
||||||
news = True
|
news = True
|
||||||
publication_type = 'newsportal'
|
|
||||||
|
title = u'Folha de S\xE3o Paulo'
|
||||||
title = u'Folha de S\xE3o Paulo'
|
__author__ = 'Euler Alves'
|
||||||
__author__ = 'Euler Alves'
|
description = u'Brazilian news from Folha de S\xE3o Paulo'
|
||||||
description = u'Brazilian news from Folha de S\xE3o Paulo'
|
publisher = u'Folha de S\xE3o Paulo'
|
||||||
publisher = u'Folha de S\xE3o Paulo'
|
category = 'news, rss'
|
||||||
category = 'news, rss'
|
|
||||||
|
oldest_article = 4
|
||||||
oldest_article = 4
|
max_articles_per_feed = 100
|
||||||
max_articles_per_feed = 100
|
summary_length = 1000
|
||||||
summary_length = 1000
|
|
||||||
|
remove_javascript = True
|
||||||
remove_javascript = True
|
no_stylesheets = True
|
||||||
no_stylesheets = True
|
use_embedded_content = False
|
||||||
use_embedded_content = False
|
remove_empty_feeds = True
|
||||||
remove_empty_feeds = True
|
timefmt = ' [%d %b %Y (%a)]'
|
||||||
timefmt = ' [%d %b %Y (%a)]'
|
|
||||||
|
html2lrf_options = [
|
||||||
html2lrf_options = [
|
'--comment', description
|
||||||
'--comment', description
|
,'--category', category
|
||||||
,'--category', category
|
,'--publisher', publisher
|
||||||
,'--publisher', publisher
|
]
|
||||||
]
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
|
||||||
|
hoje = datetime.now()
|
||||||
hoje = datetime.now()
|
pubdate = hoje.strftime('%a, %d %b')
|
||||||
pubdate = hoje.strftime('%a, %d %b')
|
if hoje.hour<6:
|
||||||
if hoje.hour<6:
|
hoje = hoje-timedelta(days=1)
|
||||||
hoje = hoje-timedelta(days=1)
|
CAPA = 'http://www1.folha.uol.com.br/fsp/images/cp'+hoje.strftime('%d%m%Y')+'.jpg'
|
||||||
CAPA = 'http://www1.folha.uol.com.br/fsp/images/cp'+hoje.strftime('%d%m%Y')+'.jpg'
|
SCREENSHOT = 'http://www1.folha.uol.com.br/'
|
||||||
SCREENSHOT = 'http://www1.folha.uol.com.br/'
|
cover_margins = (0,0,'white')
|
||||||
cover_margins = (0,0,'white')
|
masthead_url = 'http://f.i.uol.com.br/fsp/furniture/images/lgo-fsp-430x50-ffffff.gif'
|
||||||
masthead_url = 'http://f.i.uol.com.br/fsp/furniture/images/lgo-fsp-430x50-ffffff.gif'
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})]
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})]
|
remove_tags = [
|
||||||
remove_tags = [
|
dict(name='div',
|
||||||
dict(name='div',
|
attrs={'id':[
|
||||||
attrs={'id':[
|
'articleButton'
|
||||||
'articleButton'
|
,'bookmarklets'
|
||||||
,'bookmarklets'
|
,'ad-180x150-1'
|
||||||
,'ad-180x150-1'
|
,'contextualAdsArticle'
|
||||||
,'contextualAdsArticle'
|
,'articleEnd'
|
||||||
,'articleEnd'
|
,'articleComments'
|
||||||
,'articleComments'
|
]})
|
||||||
]})
|
,dict(name='div',
|
||||||
,dict(name='div',
|
attrs={'class':[
|
||||||
attrs={'class':[
|
'openBox adslibraryArticle'
|
||||||
'openBox adslibraryArticle'
|
]})
|
||||||
]})
|
|
||||||
|
,dict(name='a')
|
||||||
,dict(name='a')
|
,dict(name='iframe')
|
||||||
,dict(name='iframe')
|
,dict(name='link')
|
||||||
,dict(name='link')
|
,dict(name='script')
|
||||||
,dict(name='script')
|
]
|
||||||
]
|
|
||||||
|
feeds = [
|
||||||
feeds = [
|
(u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml')
|
||||||
(u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml')
|
,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml')
|
||||||
,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml')
|
,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml')
|
||||||
,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml')
|
,(u'Ci\xEAncia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml')
|
||||||
,(u'Ci\xEAncia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml')
|
,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml')
|
||||||
,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml')
|
,(u'Equil\xEDbrio e Sa\xFAde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml')
|
||||||
,(u'Equil\xEDbrio e Sa\xFAde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml')
|
,(u'Turismo', u'http://feeds.folha.uol.com.br/folha/turismo/rss091.xml')
|
||||||
,(u'Turismo', u'http://feeds.folha.uol.com.br/folha/turismo/rss091.xml')
|
,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml')
|
||||||
,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml')
|
,(u'Pelo Mundo', u'http://feeds.folha.uol.com.br/pelomundo.folha.rssblog.uol.com.br/')
|
||||||
,(u'Pelo Mundo', u'http://feeds.folha.uol.com.br/pelomundo.folha.rssblog.uol.com.br/')
|
,(u'Circuito integrado', u'http://feeds.folha.uol.com.br/circuitointegrado.folha.rssblog.uol.com.br/')
|
||||||
,(u'Circuito integrado', u'http://feeds.folha.uol.com.br/circuitointegrado.folha.rssblog.uol.com.br/')
|
,(u'Blog do Fred', u'http://feeds.folha.uol.com.br/blogdofred.folha.rssblog.uol.com.br/')
|
||||||
,(u'Blog do Fred', u'http://feeds.folha.uol.com.br/blogdofred.folha.rssblog.uol.com.br/')
|
,(u'Maria In\xEAs Dolci', u'http://feeds.folha.uol.com.br/mariainesdolci.folha.blog.uol.com.br/')
|
||||||
,(u'Maria In\xEAs Dolci', u'http://feeds.folha.uol.com.br/mariainesdolci.folha.blog.uol.com.br/')
|
,(u'Eduardo Ohata', u'http://feeds.folha.uol.com.br/folha/pensata/eduardoohata/rss091.xml')
|
||||||
,(u'Eduardo Ohata', u'http://feeds.folha.uol.com.br/folha/pensata/eduardoohata/rss091.xml')
|
,(u'Kennedy Alencar', u'http://feeds.folha.uol.com.br/folha/pensata/kennedyalencar/rss091.xml')
|
||||||
,(u'Kennedy Alencar', u'http://feeds.folha.uol.com.br/folha/pensata/kennedyalencar/rss091.xml')
|
,(u'Eliane Catanh\xEAde', u'http://feeds.folha.uol.com.br/folha/pensata/elianecantanhede/rss091.xml')
|
||||||
,(u'Eliane Catanh\xEAde', u'http://feeds.folha.uol.com.br/folha/pensata/elianecantanhede/rss091.xml')
|
,(u'Fernado Canzian', u'http://feeds.folha.uol.com.br/folha/pensata/fernandocanzian/rss091.xml')
|
||||||
,(u'Fernado Canzian', u'http://feeds.folha.uol.com.br/folha/pensata/fernandocanzian/rss091.xml')
|
,(u'Gilberto Dimenstein', u'http://feeds.folha.uol.com.br/folha/pensata/gilbertodimenstein/rss091.xml')
|
||||||
,(u'Gilberto Dimenstein', u'http://feeds.folha.uol.com.br/folha/pensata/gilbertodimenstein/rss091.xml')
|
,(u'H\xE9lio Schwartsman', u'http://feeds.folha.uol.com.br/folha/pensata/helioschwartsman/rss091.xml')
|
||||||
,(u'H\xE9lio Schwartsman', u'http://feeds.folha.uol.com.br/folha/pensata/helioschwartsman/rss091.xml')
|
,(u'Jo\xE3o Pereira Coutinho', u'http://http://feeds.folha.uol.com.br/folha/pensata/joaopereiracoutinho/rss091.xml')
|
||||||
,(u'Jo\xE3o Pereira Coutinho', u'http://http://feeds.folha.uol.com.br/folha/pensata/joaopereiracoutinho/rss091.xml')
|
,(u'Luiz Caversan', u'http://http://feeds.folha.uol.com.br/folha/pensata/luizcaversan/rss091.xml')
|
||||||
,(u'Luiz Caversan', u'http://http://feeds.folha.uol.com.br/folha/pensata/luizcaversan/rss091.xml')
|
,(u'S\xE9rgio Malbergier', u'http://http://feeds.folha.uol.com.br/folha/pensata/sergiomalbergier/rss091.xml')
|
||||||
,(u'S\xE9rgio Malbergier', u'http://http://feeds.folha.uol.com.br/folha/pensata/sergiomalbergier/rss091.xml')
|
,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml')
|
||||||
,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml')
|
]
|
||||||
]
|
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'title' : title
|
'title' : title
|
||||||
,'comments' : description
|
,'comments' : description
|
||||||
,'publisher' : publisher
|
,'publisher' : publisher
|
||||||
,'tags' : category
|
,'tags' : category
|
||||||
,'language' : LANGUAGE
|
,'language' : LANGUAGE
|
||||||
,'linearize_tables': True
|
,'linearize_tables': True
|
||||||
}
|
}
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
for item in soup.findAll(style=True):
|
for item in soup.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
if not soup.find(attrs={'http-equiv':'Content-Language'}):
|
if not soup.find(attrs={'http-equiv':'Content-Language'}):
|
||||||
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
|
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
|
||||||
soup.head.insert(0,meta0)
|
soup.head.insert(0,meta0)
|
||||||
if not soup.find(attrs={'http-equiv':'Content-Type'}):
|
if not soup.find(attrs={'http-equiv':'Content-Type'}):
|
||||||
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
|
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
|
||||||
soup.head.insert(0,meta1)
|
soup.head.insert(0,meta1)
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
def postprocess_html(self, soup, first):
|
def postprocess_html(self, soup, first):
|
||||||
#process all the images. assumes that the new html has the correct path
|
#process all the images. assumes that the new html has the correct path
|
||||||
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
|
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
|
||||||
iurl = tag['src']
|
iurl = tag['src']
|
||||||
img = Image()
|
img = Image()
|
||||||
img.open(iurl)
|
img.open(iurl)
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
|
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
|
||||||
pw = PixelWand()
|
if img < 0:
|
||||||
if( width > height and width > 590) :
|
raise RuntimeError('Out of memory')
|
||||||
print 'Rotate image'
|
pw = PixelWand()
|
||||||
img.rotate(pw, -90)
|
if( width > height and width > 590) :
|
||||||
img.save(iurl)
|
print 'Rotate image'
|
||||||
return soup
|
img.rotate(pw, -90)
|
||||||
|
img.save(iurl)
|
||||||
def get_cover_url(self):
|
return soup
|
||||||
cover_url = self.CAPA
|
|
||||||
pedido = Request(self.CAPA)
|
def get_cover_url(self):
|
||||||
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
|
cover_url = self.CAPA
|
||||||
pedido.add_header('Accept-Charset',self.ENCHTM)
|
pedido = Request(self.CAPA)
|
||||||
pedido.add_header('Referer',self.SCREENSHOT)
|
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
|
||||||
try:
|
pedido.add_header('Accept-Charset',self.ENCHTM)
|
||||||
resposta = urlopen(pedido)
|
pedido.add_header('Referer',self.SCREENSHOT)
|
||||||
soup = BeautifulSoup(resposta)
|
try:
|
||||||
cover_item = soup.find('body')
|
resposta = urlopen(pedido)
|
||||||
if cover_item:
|
soup = BeautifulSoup(resposta)
|
||||||
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
cover_item = soup.find('body')
|
||||||
return cover_url
|
if cover_item:
|
||||||
except URLError:
|
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
||||||
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
return cover_url
|
||||||
return cover_url
|
except URLError:
|
||||||
|
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
|
||||||
|
return cover_url
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 714 B |
BIN
recipes/icons/folhadesaopaulo.png
Normal file
BIN
recipes/icons/folhadesaopaulo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 392 B |
BIN
recipes/icons/prostamerika.png
Normal file
BIN
recipes/icons/prostamerika.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 561 B |
BIN
recipes/icons/sb_nation.png
Normal file
BIN
recipes/icons/sb_nation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
recipes/icons/wvhooligan.png
Normal file
BIN
recipes/icons/wvhooligan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = "2008, Derry FitzGerald. 2009 Modified by Ray Kinsella and David O'Callaghan"
|
__copyright__ = "2008, Derry FitzGerald. 2009 Modified by Ray Kinsella and David O'Callaghan, 2011 Modified by Phil Burns"
|
||||||
'''
|
'''
|
||||||
irishtimes.com
|
irishtimes.com
|
||||||
'''
|
'''
|
||||||
@ -9,17 +9,20 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
|
|
||||||
class IrishTimes(BasicNewsRecipe):
|
class IrishTimes(BasicNewsRecipe):
|
||||||
title = u'The Irish Times'
|
title = u'The Irish Times'
|
||||||
__author__ = "Derry FitzGerald, Ray Kinsella and David O'Callaghan"
|
encoding = 'ISO-8859-15'
|
||||||
|
__author__ = "Derry FitzGerald, Ray Kinsella, David O'Callaghan and Phil Burns"
|
||||||
language = 'en_IE'
|
language = 'en_IE'
|
||||||
timefmt = ' (%A, %B %d, %Y)'
|
timefmt = ' (%A, %B %d, %Y)'
|
||||||
|
|
||||||
oldest_article = 3
|
|
||||||
|
oldest_article = 1.0
|
||||||
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
simultaneous_downloads= 1
|
simultaneous_downloads= 5
|
||||||
|
|
||||||
r = re.compile('.*(?P<url>http:\/\/(www.irishtimes.com)|(rss.feedsportal.com\/c)\/.*\.html?).*')
|
r = re.compile('.*(?P<url>http:\/\/(www.irishtimes.com)|(rss.feedsportal.com\/c)\/.*\.html?).*')
|
||||||
remove_tags = [dict(name='div', attrs={'class':'footer'})]
|
remove_tags = [dict(name='div', attrs={'class':'footer'})]
|
||||||
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
|
extra_css = 'p, div { margin: 0pt; border: 0pt; text-indent: 0.5em } .headline {font-size: large;} \n .fact { padding-top: 10pt }'
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
('Frontpage', 'http://www.irishtimes.com/feeds/rss/newspaper/index.rss'),
|
('Frontpage', 'http://www.irishtimes.com/feeds/rss/newspaper/index.rss'),
|
||||||
@ -30,15 +33,29 @@ class IrishTimes(BasicNewsRecipe):
|
|||||||
('Sport', 'http://www.irishtimes.com/feeds/rss/newspaper/sport.rss'),
|
('Sport', 'http://www.irishtimes.com/feeds/rss/newspaper/sport.rss'),
|
||||||
('Opinion', 'http://www.irishtimes.com/feeds/rss/newspaper/opinion.rss'),
|
('Opinion', 'http://www.irishtimes.com/feeds/rss/newspaper/opinion.rss'),
|
||||||
('Letters', 'http://www.irishtimes.com/feeds/rss/newspaper/letters.rss'),
|
('Letters', 'http://www.irishtimes.com/feeds/rss/newspaper/letters.rss'),
|
||||||
|
('Magazine', 'http://www.irishtimes.com/feeds/rss/newspaper/magazine.rss'),
|
||||||
|
('Health', 'http://www.irishtimes.com/feeds/rss/newspaper/health.rss'),
|
||||||
|
('Education & Parenting', 'http://www.irishtimes.com/feeds/rss/newspaper/education.rss'),
|
||||||
|
('Motors', 'http://www.irishtimes.com/feeds/rss/newspaper/motors.rss'),
|
||||||
|
('An Teanga Bheo', 'http://www.irishtimes.com/feeds/rss/newspaper/anteangabheo.rss'),
|
||||||
|
('Commercial Property', 'http://www.irishtimes.com/feeds/rss/newspaper/commercialproperty.rss'),
|
||||||
|
('Science Today', 'http://www.irishtimes.com/feeds/rss/newspaper/sciencetoday.rss'),
|
||||||
|
('Property', 'http://www.irishtimes.com/feeds/rss/newspaper/property.rss'),
|
||||||
|
('The Tickets', 'http://www.irishtimes.com/feeds/rss/newspaper/theticket.rss'),
|
||||||
|
('Weekend', 'http://www.irishtimes.com/feeds/rss/newspaper/weekend.rss'),
|
||||||
|
('News features', 'http://www.irishtimes.com/feeds/rss/newspaper/newsfeatures.rss'),
|
||||||
|
('Obituaries', 'http://www.irishtimes.com/feeds/rss/newspaper/obituaries.rss'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
if url.count('rss.feedsportal.com'):
|
if url.count('rss.feedsportal.com'):
|
||||||
u = 'http://www.irishtimes.com' + \
|
u = url.replace('0Bhtml/story01.htm','_pf0Bhtml/story01.htm')
|
||||||
(((url[70:].replace('0C','/')).replace('0A','0'))).replace('0Bhtml/story01.htm','_pf.html')
|
else:
|
||||||
else:
|
u = url.replace('.html','_pf.html')
|
||||||
u = url.replace('.html','_pf.html')
|
return u
|
||||||
return u
|
|
||||||
|
|
||||||
def get_article_url(self, article):
|
def get_article_url(self, article):
|
||||||
return article.link
|
return article.link
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,37 +1,100 @@
|
|||||||
__license__ = 'GPL v3'
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
__copyright__ = '2010, NA'
|
from datetime import datetime
|
||||||
'''
|
from calibre.ebooks.BeautifulSoup import Tag
|
||||||
lifehacker.com
|
from calibre.utils.magick import Image, PixelWand
|
||||||
'''
|
|
||||||
|
class LifeHacker(BasicNewsRecipe):
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
|
||||||
|
LANGUAGE = 'en'
|
||||||
class Lifehacker(BasicNewsRecipe):
|
LANGHTM = 'en'
|
||||||
title = 'Lifehacker'
|
language = 'en'
|
||||||
__author__ = 'Kovid Goyal'
|
ENCODING = 'utf'
|
||||||
description = "Computers make us more productive. Yeah, right. Lifehacker recommends the software downloads and web sites that actually save time. Don't live to geek; geek to live."
|
ENCHTM = 'utf-8'
|
||||||
publisher = 'lifehacker.com'
|
requires_version = (0,7,47)
|
||||||
category = 'news, IT, Internet, gadgets, tips and tricks, howto, diy'
|
news = True
|
||||||
oldest_article = 2
|
|
||||||
max_articles_per_feed = 100
|
title = u'LifeHacker'
|
||||||
no_stylesheets = True
|
__author__ = 'Euler Alves'
|
||||||
encoding = 'utf-8'
|
description = u'Tips, tricks, and downloads for getting things done.'
|
||||||
use_embedded_content = True
|
publisher = u'lifehacker.com'
|
||||||
language = 'en'
|
author = u'Adam Pash & Kevin Purdy & Adam Dachis & Whitson Gordon & Gina Trapani'
|
||||||
masthead_url = 'http://cache.gawkerassets.com/assets/lifehacker.com/img/logo.png'
|
category = 'news, rss'
|
||||||
conversion_options = {
|
|
||||||
'comment' : description
|
oldest_article = 4
|
||||||
, 'tags' : category
|
max_articles_per_feed = 20
|
||||||
, 'publisher' : publisher
|
summary_length = 1000
|
||||||
, 'language' : language
|
|
||||||
}
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
remove_tags = [
|
use_embedded_content = True
|
||||||
{'class': 'feedflare'},
|
remove_empty_feeds = True
|
||||||
]
|
timefmt = ' [%d %b %Y (%a)]'
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/vip?format=xml')]
|
hoje = datetime.now()
|
||||||
|
pubdate = hoje.strftime('%a, %d %b')
|
||||||
def preprocess_html(self, soup):
|
cover_url = 'http://api.thumbalizr.com/?api_key='+THUMBALIZR_API+'&url=http://lifehacker.com&width=600&quality=90'
|
||||||
return self.adeify_images(soup)
|
cover_margins = (0,0,'white')
|
||||||
|
masthead_url = 'http://cache.gawkerassets.com/assets/lifehacker.com/img/logo.png'
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
{'class': 'feedflare'},
|
||||||
|
dict(name='div',
|
||||||
|
attrs={'class':[
|
||||||
|
'ad_container'
|
||||||
|
,'ad_300x250'
|
||||||
|
,'ad_interstitial'
|
||||||
|
,'share-wrap'
|
||||||
|
,'ad_300x600'
|
||||||
|
,'ad_perma-footer-adsense'
|
||||||
|
,'ad_perma-panorama'
|
||||||
|
,'ad panorama'
|
||||||
|
,'ad_container'
|
||||||
|
]})
|
||||||
|
,dict(name='div',
|
||||||
|
attrs={'id':[
|
||||||
|
'agegate_container'
|
||||||
|
,'agegate_container_rejected'
|
||||||
|
,'sharemenu-wrap'
|
||||||
|
]})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/vip?format=xml')]
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'title' : title
|
||||||
|
,'comments' : description
|
||||||
|
,'publisher' : publisher
|
||||||
|
,'tags' : category
|
||||||
|
,'language' : LANGUAGE
|
||||||
|
,'linearize_tables': True
|
||||||
|
}
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
if not soup.find(attrs={'http-equiv':'Content-Language'}):
|
||||||
|
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
|
||||||
|
soup.head.insert(0,meta0)
|
||||||
|
if not soup.find(attrs={'http-equiv':'Content-Type'}):
|
||||||
|
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
|
||||||
|
soup.head.insert(0,meta1)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def postprocess_html(self, soup, first):
|
||||||
|
#process all the images. assumes that the new html has the correct path
|
||||||
|
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
|
||||||
|
iurl = tag['src']
|
||||||
|
img = Image()
|
||||||
|
img.open(iurl)
|
||||||
|
width, height = img.size
|
||||||
|
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
|
||||||
|
if img < 0:
|
||||||
|
raise RuntimeError('Out of memory')
|
||||||
|
pw = PixelWand()
|
||||||
|
if( width > height and width > 590) :
|
||||||
|
print 'Rotate image'
|
||||||
|
img.rotate(pw, -90)
|
||||||
|
img.save(iurl)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
68
recipes/prostamerika.recipe
Normal file
68
recipes/prostamerika.recipe
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = 'zotzo'
|
||||||
|
|
||||||
|
"""
|
||||||
|
http://www.prostamerika.com/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
|
||||||
|
class ProstAmerika(BasicNewsRecipe):
|
||||||
|
title = 'Prost Amerika'
|
||||||
|
language = 'en'
|
||||||
|
__author__ = 'rylsfan'
|
||||||
|
#authors =
|
||||||
|
description = 'Seattle soccer with a European accent. News, features, and match reports.'
|
||||||
|
publisher = 'ProstAmerika' # 4464 fremont avenue n, # 209, Seattle, 98103, United States
|
||||||
|
category = 'Sports'
|
||||||
|
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
cover_url = 'http://img17.imageshack.us/img17/9498/prostamerika.jpg'
|
||||||
|
masthead_url = 'http://www.prostamerika.com/soundersfc/wp-content/uploads/2011/02/PASoccer_taglinewhole.jpg'
|
||||||
|
|
||||||
|
encoding = 'utf-8'
|
||||||
|
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
remove_javascript = True
|
||||||
|
|
||||||
|
feeds =[
|
||||||
|
(u'Cascadia', u'http://www.prostamerika.com/category/localfootball/feed/' ),
|
||||||
|
(u'MLS', u'http://www.prostamerika.com/category/mls/feed/'),
|
||||||
|
(u'EPL', u'http://www.prostamerika.com/category/epl/feed/'),
|
||||||
|
(u'World', u'http://www.prostamerika.com/category/international-soccer/feed/'),
|
||||||
|
(u'Fan Culture',u'http://www.prostamerika.com/category/fan-culture/feed/')
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':'maincontent'})]
|
||||||
|
remove_tags = [
|
||||||
|
{'class':'tweetmeme_button'},
|
||||||
|
{'class':'wp-caption-text'}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags_after =[
|
||||||
|
{'class':'tweetmeme_button'}
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Didot,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
56
recipes/sb_nation.recipe
Normal file
56
recipes/sb_nation.recipe
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = 'Zotzo'
|
||||||
|
'''
|
||||||
|
http://www.stumptownfooty.com/
|
||||||
|
http://www.eightysixforever.com
|
||||||
|
http://www.sounderatheart.com
|
||||||
|
http://www.dailysoccerfix.com/
|
||||||
|
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class SBNation(BasicNewsRecipe):
|
||||||
|
title = u'SBNation'
|
||||||
|
__author__ = 'rylsfan'
|
||||||
|
description = u"More than 290 individual communities, each offering high quality year-round coverage and conversation led by fans who are passionate."
|
||||||
|
oldest_article = 3
|
||||||
|
language = 'en'
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
#cover_url = 'http://img132.imageshack.us/img132/4913/2hyggjegqqdywzn9.png'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h2', attrs={'class':'title'})
|
||||||
|
,dict(name='div', attrs={'class':'entry-body'})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_after = dict(name='div', attrs={'class':'footline entry-actions'})
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':'footline entry-actions'}),
|
||||||
|
{'class': 'extend-divide'}
|
||||||
|
]
|
||||||
|
# SBNation has 300 special blogs to choose from. These are just a couple!
|
||||||
|
feeds = [
|
||||||
|
(u'Daily Fix', u'http://www.dailysoccerfix.com/rss/'),
|
||||||
|
(u"Stumptown Footy", u'http://www.stumptownfooty.com/rss/'),
|
||||||
|
(u'Sounders', u'http://www.sounderatheart.com/rss/'),
|
||||||
|
(u'Whitecaps', u'http://www.eightysixforever.com/rss/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = """
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h2{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
p{font-family:Helvetica,sans-serif; display: block; text-align: left; text-decoration: none; text-indent: 0%;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
def populate_article_metadata(self, article, soup, first):
|
||||||
|
h2 = soup.find('h2')
|
||||||
|
h2.replaceWith(h2.prettify() + '<p><em> By ' + article.author + '</em></p>')
|
61
recipes/wvhooligan.recipe
Normal file
61
recipes/wvhooligan.recipe
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = 'zotzo'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
'''
|
||||||
|
http://wvhooligan.com/
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
#import re
|
||||||
|
|
||||||
|
class wvHooligan(BasicNewsRecipe):
|
||||||
|
authors = u'Drew Epperley'
|
||||||
|
__author__ = 'rylsfan'
|
||||||
|
language = 'en'
|
||||||
|
version = 2
|
||||||
|
|
||||||
|
title = u'WV Hooligan'
|
||||||
|
publisher = u'Drew Epperley'
|
||||||
|
publication_type = 'Blog'
|
||||||
|
category = u'Soccer'
|
||||||
|
description = u'A look at Major League Soccer (MLS) through the eyes of a MLS writer and fan.'
|
||||||
|
|
||||||
|
cover_url = 'http://wvhooligan.com/wp-content/themes/urbanelements/images/logo3.png'
|
||||||
|
|
||||||
|
oldest_article = 15
|
||||||
|
max_articles_per_feed = 150
|
||||||
|
use_embedded_content = True
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
{'class': 'feedflare'},
|
||||||
|
{'class': 'tweetmeme_button'},
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
feeds =[
|
||||||
|
(u'Stories', u'http://feeds2.feedburner.com/wvhooligan'),
|
||||||
|
(u'MLS', u'http://wvhooligan.com/category/mls/feed/'),
|
||||||
|
(u'MLS Power Rankings', u'http://wvhooligan.com/category/power-rankings/feed/'),
|
||||||
|
(u'MLS Expansion', u'http://wvhooligan.com/category/mls/expansion-talk/feed/'),
|
||||||
|
(u'US National Team', u'http://wvhooligan.com/category/us-national-team/feed/'),
|
||||||
|
(u'College', u'http://wvhooligan.com/category/college-soccer/feed/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,7 +5,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, glob
|
import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, \
|
||||||
|
glob, stat
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
from tempfile import NamedTemporaryFile, mkdtemp
|
from tempfile import NamedTemporaryFile, mkdtemp
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -344,6 +345,8 @@ class UploadUserManual(Command): # {{{
|
|||||||
def build_plugin_example(self, path):
|
def build_plugin_example(self, path):
|
||||||
from calibre import CurrentDir
|
from calibre import CurrentDir
|
||||||
with NamedTemporaryFile(suffix='.zip') as f:
|
with NamedTemporaryFile(suffix='.zip') as f:
|
||||||
|
os.fchmod(f.fileno(),
|
||||||
|
stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE)
|
||||||
with CurrentDir(self.d(path)):
|
with CurrentDir(self.d(path)):
|
||||||
with ZipFile(f, 'w') as zf:
|
with ZipFile(f, 'w') as zf:
|
||||||
for x in os.listdir('.'):
|
for x in os.listdir('.'):
|
||||||
@ -352,8 +355,8 @@ class UploadUserManual(Command): # {{{
|
|||||||
for y in os.listdir(x):
|
for y in os.listdir(x):
|
||||||
zf.write(os.path.join(x, y))
|
zf.write(os.path.join(x, y))
|
||||||
bname = self.b(path) + '_plugin.zip'
|
bname = self.b(path) + '_plugin.zip'
|
||||||
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS,
|
dest = '%s/%s'%(DOWNLOADS, bname)
|
||||||
bname)])
|
subprocess.check_call(['scp', f.name, dest])
|
||||||
|
|
||||||
def run(self, opts):
|
def run(self, opts):
|
||||||
path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples')
|
path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples')
|
||||||
|
@ -61,6 +61,9 @@ def osx_version():
|
|||||||
if m:
|
if m:
|
||||||
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||||
|
|
||||||
|
def confirm_config_name(name):
|
||||||
|
return name + '_again'
|
||||||
|
|
||||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||||
_filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<',
|
_filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<',
|
||||||
u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32))))
|
u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32))))
|
||||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time
|
import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time
|
||||||
from calibre.constants import __appname__, __version__, DEBUG
|
from calibre.constants import __appname__, __version__, DEBUG
|
||||||
from calibre import fit_image
|
from calibre import fit_image, confirm_config_name
|
||||||
from calibre.constants import isosx, iswindows
|
from calibre.constants import isosx, iswindows
|
||||||
from calibre.devices.errors import OpenFeedback, UserFeedback
|
from calibre.devices.errors import OpenFeedback, UserFeedback
|
||||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
||||||
@ -18,12 +18,77 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation, \
|
|||||||
from calibre.ebooks.metadata.book.base import Metadata
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.ebooks.metadata.epub import set_metadata
|
from calibre.ebooks.metadata.epub import set_metadata
|
||||||
from calibre.library.server.utils import strftime
|
from calibre.library.server.utils import strftime
|
||||||
from calibre.utils.config import config_dir, prefs
|
from calibre.utils.config import config_dir, dynamic, prefs
|
||||||
from calibre.utils.date import now, parse_date
|
from calibre.utils.date import now, parse_date
|
||||||
from calibre.utils.logging import Log
|
from calibre.utils.logging import Log
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
|
||||||
|
|
||||||
|
class AppleOpenFeedback(OpenFeedback):
|
||||||
|
|
||||||
|
def __init__(self, plugin):
|
||||||
|
OpenFeedback.__init__(self, u'')
|
||||||
|
self.log = plugin.log
|
||||||
|
self.plugin = plugin
|
||||||
|
|
||||||
|
def custom_dialog(self, parent):
|
||||||
|
from PyQt4.Qt import (QDialog, QDialogButtonBox, QIcon,
|
||||||
|
QLabel, QPushButton, QVBoxLayout)
|
||||||
|
|
||||||
|
class Dialog(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, p, cd, pixmap='dialog_information.png'):
|
||||||
|
QDialog.__init__(self, p)
|
||||||
|
self.cd = cd
|
||||||
|
self.setWindowTitle("Apple iDevice detected")
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
msg = QLabel()
|
||||||
|
msg.setText(_(
|
||||||
|
'<p>If you do not want calibre to recognize your Apple iDevice '
|
||||||
|
'when it is connected to your computer, '
|
||||||
|
'click <b>Disable Apple Driver</b>.</p>'
|
||||||
|
'<p>To transfer books to your iDevice, '
|
||||||
|
'click <b>Disable Apple Driver</b>, '
|
||||||
|
"then use the 'Connect to iTunes' method recommended in the "
|
||||||
|
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">Calibre + iDevices FAQ</a>, '
|
||||||
|
'using the <em>Connect/Share</em>|<em>Connect to iTunes</em> menu item.</p>'
|
||||||
|
'<p>Enabling the Apple driver for direct connection to iDevices '
|
||||||
|
'is an unsupported advanced user mode.</p>'
|
||||||
|
'<p></p>'
|
||||||
|
))
|
||||||
|
msg.setOpenExternalLinks(True)
|
||||||
|
msg.setWordWrap(True)
|
||||||
|
l.addWidget(msg)
|
||||||
|
|
||||||
|
self.bb = QDialogButtonBox()
|
||||||
|
disable_driver = QPushButton(_("Disable Apple driver"))
|
||||||
|
disable_driver.setDefault(True)
|
||||||
|
self.bb.addButton(disable_driver, QDialogButtonBox.RejectRole)
|
||||||
|
|
||||||
|
enable_driver = QPushButton(_("Enable Apple driver"))
|
||||||
|
self.bb.addButton(enable_driver, QDialogButtonBox.AcceptRole)
|
||||||
|
l.addWidget(self.bb)
|
||||||
|
self.bb.accepted.connect(self.accept)
|
||||||
|
self.bb.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
self.setWindowIcon(QIcon(I(pixmap)))
|
||||||
|
self.resize(self.sizeHint())
|
||||||
|
|
||||||
|
self.finished.connect(self.do_it)
|
||||||
|
|
||||||
|
def do_it(self, return_code):
|
||||||
|
if return_code == self.Accepted:
|
||||||
|
self.cd.log.info(" Apple driver ENABLED")
|
||||||
|
dynamic[confirm_config_name(self.cd.plugin.DISPLAY_DISABLE_DIALOG)] = False
|
||||||
|
else:
|
||||||
|
from calibre.customize.ui import disable_plugin
|
||||||
|
self.cd.log.info(" Apple driver DISABLED")
|
||||||
|
disable_plugin(self.cd.plugin)
|
||||||
|
|
||||||
|
return Dialog(parent, self)
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
@ -54,15 +119,11 @@ class DriverBase(DeviceConfig, DevicePlugin):
|
|||||||
'iBooks Category'),
|
'iBooks Category'),
|
||||||
_('Cache covers from iTunes/iBooks') +
|
_('Cache covers from iTunes/iBooks') +
|
||||||
':::' +
|
':::' +
|
||||||
_('Enable to cache and display covers from iTunes/iBooks'),
|
_('Enable to cache and display covers from iTunes/iBooks')
|
||||||
_("Skip 'Connect to iTunes' recommendation") +
|
|
||||||
':::' +
|
|
||||||
_("Enable to skip the 'Connect to iTunes' recommendation dialog")
|
|
||||||
]
|
]
|
||||||
EXTRA_CUSTOMIZATION_DEFAULT = [
|
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||||
True,
|
True,
|
||||||
True,
|
True,
|
||||||
False,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -118,12 +179,13 @@ class ITUNES(DriverBase):
|
|||||||
supported_platforms = ['osx','windows']
|
supported_platforms = ['osx','windows']
|
||||||
author = 'GRiker'
|
author = 'GRiker'
|
||||||
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
||||||
version = (0,9,0)
|
version = (1,0,0)
|
||||||
|
|
||||||
|
DISPLAY_DISABLE_DIALOG = "display_disable_apple_driver_dialog"
|
||||||
|
|
||||||
# EXTRA_CUSTOMIZATION_MESSAGE indexes
|
# EXTRA_CUSTOMIZATION_MESSAGE indexes
|
||||||
USE_SERIES_AS_CATEGORY = 0
|
USE_SERIES_AS_CATEGORY = 0
|
||||||
CACHE_COVERS = 1
|
CACHE_COVERS = 1
|
||||||
SKIP_CONNECT_TO_ITUNES_DIALOG = 2
|
|
||||||
|
|
||||||
OPEN_FEEDBACK_MESSAGE = _(
|
OPEN_FEEDBACK_MESSAGE = _(
|
||||||
'Apple device detected, launching iTunes, please wait ...')
|
'Apple device detected, launching iTunes, please wait ...')
|
||||||
@ -739,21 +801,17 @@ class ITUNES(DriverBase):
|
|||||||
Note that most of the initialization is necessarily performed in can_handle(), as
|
Note that most of the initialization is necessarily performed in can_handle(), as
|
||||||
we need to talk to iTunes to discover if there's a connected iPod
|
we need to talk to iTunes to discover if there's a connected iPod
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES.open()")
|
self.log.info("ITUNES.open()")
|
||||||
|
|
||||||
# Display a dialog recommending using 'Connect to iTunes'
|
# Display a dialog recommending using 'Connect to iTunes' if user hasn't
|
||||||
if False and not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]:
|
# previously disabled the dialog
|
||||||
raise OpenFeedback('<p>' + ('Click the "Connect/Share" button and choose'
|
if dynamic.get(confirm_config_name(self.DISPLAY_DISABLE_DIALOG),True):
|
||||||
' "Connect to iTunes" to send books from your calibre library'
|
raise AppleOpenFeedback(self)
|
||||||
' to your Apple iDevice.<p>For more information, see '
|
else:
|
||||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">'
|
if DEBUG:
|
||||||
'Calibre + Apple iDevices FAQ</a>.<p>'
|
self.log.info(" advanced user mode, directly connecting to iDevice")
|
||||||
'After following the Quick Start steps outlined in the FAQ, '
|
|
||||||
'restart calibre.'))
|
|
||||||
|
|
||||||
if DEBUG:
|
|
||||||
self.log.info(" advanced user mode, directly connecting to iDevice")
|
|
||||||
|
|
||||||
# Confirm/create thumbs archive
|
# Confirm/create thumbs archive
|
||||||
if not os.path.exists(self.cache_dir):
|
if not os.path.exists(self.cache_dir):
|
||||||
|
@ -10,7 +10,7 @@ Sanda library wrapper
|
|||||||
|
|
||||||
import ctypes, uuid, hashlib, os, sys
|
import ctypes, uuid, hashlib, os, sys
|
||||||
from threading import Event, Lock
|
from threading import Event, Lock
|
||||||
from calibre.constants import iswindows, islinux, isosx
|
from calibre.constants import iswindows
|
||||||
from calibre import load_library
|
from calibre import load_library
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -29,12 +29,9 @@ try:
|
|||||||
except:
|
except:
|
||||||
lib_handle = None
|
lib_handle = None
|
||||||
|
|
||||||
|
text_encoding = 'utf-8'
|
||||||
if iswindows:
|
if iswindows:
|
||||||
text_encoding = 'mbcs'
|
text_encoding = 'mbcs'
|
||||||
elif islinux:
|
|
||||||
text_encoding = 'utf-8'
|
|
||||||
elif isosx:
|
|
||||||
text_encoding = 'utf-8'
|
|
||||||
|
|
||||||
def is_bambook_lib_ready():
|
def is_bambook_lib_ready():
|
||||||
return lib_handle != None
|
return lib_handle != None
|
||||||
|
@ -41,6 +41,13 @@ class OpenFeedback(DeviceError):
|
|||||||
self.feedback_msg = msg
|
self.feedback_msg = msg
|
||||||
DeviceError.__init__(self, msg)
|
DeviceError.__init__(self, msg)
|
||||||
|
|
||||||
|
def custom_dialog(self, parent):
|
||||||
|
'''
|
||||||
|
If you need to show the user a custom dialog, instead of just
|
||||||
|
displaying the feedback_msg, create and return it here.
|
||||||
|
'''
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
class DeviceBusy(ProtocolError):
|
class DeviceBusy(ProtocolError):
|
||||||
""" Raised when device is busy """
|
""" Raised when device is busy """
|
||||||
def __init__(self, uerr=""):
|
def __init__(self, uerr=""):
|
||||||
|
@ -110,4 +110,11 @@ def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False,
|
|||||||
if resolve_entities:
|
if resolve_entities:
|
||||||
raw = substitute_entites(raw)
|
raw = substitute_entites(raw)
|
||||||
|
|
||||||
|
if encoding and encoding.lower().replace('_', '-').strip() in (
|
||||||
|
'gb2312', 'chinese', 'csiso58gb231280', 'euc-cn', 'euccn',
|
||||||
|
'eucgb2312-cn', 'gb2312-1980', 'gb2312-80', 'iso-ir-58'):
|
||||||
|
# Microsoft Word exports to HTML with encoding incorrectly set to
|
||||||
|
# gb2312 instead of gbk. gbk is a superset of gb2312, anyway.
|
||||||
|
encoding = 'gbk'
|
||||||
|
|
||||||
return raw, encoding
|
return raw, encoding
|
||||||
|
@ -1003,8 +1003,10 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
self.opts.insert_blank_line = oibl
|
self.opts.insert_blank_line = oibl
|
||||||
self.opts.remove_paragraph_spacing = orps
|
self.opts.remove_paragraph_spacing = orps
|
||||||
|
|
||||||
from calibre.ebooks.oeb.transforms.page_margin import RemoveFakeMargins
|
from calibre.ebooks.oeb.transforms.page_margin import \
|
||||||
|
RemoveFakeMargins, RemoveAdobeMargins
|
||||||
RemoveFakeMargins()(self.oeb, self.log, self.opts)
|
RemoveFakeMargins()(self.oeb, self.log, self.opts)
|
||||||
|
RemoveAdobeMargins()(self.oeb, self.log, self.opts)
|
||||||
|
|
||||||
pr(0.9)
|
pr(0.9)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, textwrap
|
import os, textwrap, sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -413,7 +413,12 @@ class LRFInput(InputFormatPlugin):
|
|||||||
('calibre', 'image-block'): image_block,
|
('calibre', 'image-block'): image_block,
|
||||||
}
|
}
|
||||||
transform = etree.XSLT(styledoc, extensions=extensions)
|
transform = etree.XSLT(styledoc, extensions=extensions)
|
||||||
result = transform(doc)
|
try:
|
||||||
|
result = transform(doc)
|
||||||
|
except RuntimeError:
|
||||||
|
sys.setrecursionlimit(5000)
|
||||||
|
result = transform(doc)
|
||||||
|
|
||||||
with open('content.opf', 'wb') as f:
|
with open('content.opf', 'wb') as f:
|
||||||
f.write(result)
|
f.write(result)
|
||||||
styles.write()
|
styles.write()
|
||||||
|
@ -198,8 +198,10 @@ class Metadata(object):
|
|||||||
return copy.deepcopy(ans)
|
return copy.deepcopy(ans)
|
||||||
|
|
||||||
def _clean_identifier(self, typ, val):
|
def _clean_identifier(self, typ, val):
|
||||||
typ = icu_lower(typ).strip().replace(':', '').replace(',', '')
|
if typ:
|
||||||
val = val.strip().replace(',', '|').replace(':', '|')
|
typ = icu_lower(typ).strip().replace(':', '').replace(',', '')
|
||||||
|
if val:
|
||||||
|
val = val.strip().replace(',', '|').replace(':', '|')
|
||||||
return typ, val
|
return typ, val
|
||||||
|
|
||||||
def set_identifiers(self, identifiers):
|
def set_identifiers(self, identifiers):
|
||||||
|
@ -182,6 +182,19 @@ def metadata_from_filename(name, pat=None):
|
|||||||
mi.isbn = si
|
mi.isbn = si
|
||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
publisher = match.group('publisher')
|
||||||
|
mi.publisher = publisher
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
pubdate = match.group('published')
|
||||||
|
if pubdate:
|
||||||
|
from calibre.utils.date import parse_date
|
||||||
|
mi.pubdate = parse_date(pubdate)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if mi.is_null('title'):
|
if mi.is_null('title'):
|
||||||
mi.title = name
|
mi.title = name
|
||||||
return mi
|
return mi
|
||||||
|
@ -483,7 +483,6 @@ class Amazon(Source):
|
|||||||
log.exception('Failed to download cover from:', cached_url)
|
log.exception('Failed to download cover from:', cached_url)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__': # tests {{{
|
if __name__ == '__main__': # tests {{{
|
||||||
# To run these test use: calibre-debug -e
|
# To run these test use: calibre-debug -e
|
||||||
# src/calibre/ebooks/metadata/sources/amazon.py
|
# src/calibre/ebooks/metadata/sources/amazon.py
|
||||||
@ -504,7 +503,7 @@ if __name__ == '__main__': # tests {{{
|
|||||||
( # This isbn not on amazon
|
( # This isbn not on amazon
|
||||||
{'identifiers':{'isbn': '8324616489'}, 'title':'Learning Python',
|
{'identifiers':{'isbn': '8324616489'}, 'title':'Learning Python',
|
||||||
'authors':['Lutz']},
|
'authors':['Lutz']},
|
||||||
[title_test('Learning Python: Powerful Object-Oriented Programming',
|
[title_test('Learning Python, 3rd Edition',
|
||||||
exact=True), authors_test(['Mark Lutz'])
|
exact=True), authors_test(['Mark Lutz'])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from calibre.customize import Plugin
|
|||||||
from calibre.utils.logging import ThreadSafeLog, FileStream
|
from calibre.utils.logging import ThreadSafeLog, FileStream
|
||||||
from calibre.utils.config import JSONConfig
|
from calibre.utils.config import JSONConfig
|
||||||
from calibre.utils.titlecase import titlecase
|
from calibre.utils.titlecase import titlecase
|
||||||
|
from calibre.ebooks.metadata import check_isbn
|
||||||
|
|
||||||
msprefs = JSONConfig('metadata_sources.json')
|
msprefs = JSONConfig('metadata_sources.json')
|
||||||
|
|
||||||
@ -244,6 +245,7 @@ class Source(Plugin):
|
|||||||
mi.title = fixcase(mi.title)
|
mi.title = fixcase(mi.title)
|
||||||
mi.authors = list(map(fixcase, mi.authors))
|
mi.authors = list(map(fixcase, mi.authors))
|
||||||
mi.tags = list(map(fixcase, mi.tags))
|
mi.tags = list(map(fixcase, mi.tags))
|
||||||
|
mi.isbn = check_isbn(mi.isbn)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from io import BytesIO
|
|||||||
|
|
||||||
from calibre.customize.ui import metadata_plugins
|
from calibre.customize.ui import metadata_plugins
|
||||||
from calibre.ebooks.metadata.sources.base import create_log
|
from calibre.ebooks.metadata.sources.base import create_log
|
||||||
|
from calibre.ebooks.metadata.xisbn import xisbn
|
||||||
|
|
||||||
# How long to wait for more results after first result is found
|
# How long to wait for more results after first result is found
|
||||||
WAIT_AFTER_FIRST_RESULT = 30 # seconds
|
WAIT_AFTER_FIRST_RESULT = 30 # seconds
|
||||||
@ -42,6 +43,7 @@ def is_worker_alive(workers):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
|
def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
|
||||||
|
start_time = time.time()
|
||||||
plugins = list(metadata_plugins['identify'])
|
plugins = list(metadata_plugins['identify'])
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
@ -79,7 +81,7 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
|
|||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
if get_results() and first_result_at is None:
|
if get_results() and first_result_at is None:
|
||||||
first_result_at = time.time()
|
first_result_at = time.time()
|
||||||
|
|
||||||
if not is_worker_alive(workers):
|
if not is_worker_alive(workers):
|
||||||
break
|
break
|
||||||
@ -105,3 +107,55 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
|
|||||||
log(plog)
|
log(plog)
|
||||||
log('\n'+'*'*80)
|
log('\n'+'*'*80)
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
result.relevance_in_source = i
|
||||||
|
result.has_cached_cover_url = \
|
||||||
|
plugin.get_cached_cover_url(result.identifiers) is not None
|
||||||
|
result.identify_plugin = plugin
|
||||||
|
|
||||||
|
log('The identify phase took %.2f seconds'%(time.time() - start_time))
|
||||||
|
log('Merging results from different sources and finding earliest',
|
||||||
|
'publication dates')
|
||||||
|
start_time = time.time()
|
||||||
|
merged_results = merge_identify_results(results, log)
|
||||||
|
log('We have %d merged results, merging took: %.2f seconds' %
|
||||||
|
(len(merged_results), time.time() - start_time))
|
||||||
|
|
||||||
|
class ISBNMerge(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pools = {}
|
||||||
|
|
||||||
|
def isbn_in_pool(self, isbn):
|
||||||
|
if isbn:
|
||||||
|
for p in self.pools:
|
||||||
|
if isbn in p:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
def pool_has_result_from_same_source(self, pool, result):
|
||||||
|
results = self.pools[pool][1]
|
||||||
|
for r in results:
|
||||||
|
if r.identify_plugin is result.identify_plugin:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_result(self, result, isbn):
|
||||||
|
pool = self.isbn_in_pool(isbn)
|
||||||
|
if pool is None:
|
||||||
|
isbns, min_year = xisbn.get_isbn_pool(isbn)
|
||||||
|
if not isbns:
|
||||||
|
isbns = frozenset([isbn])
|
||||||
|
self.pool[isbns] = pool = (min_year, [])
|
||||||
|
|
||||||
|
if not self.pool_has_result_from_same_source(pool, result):
|
||||||
|
pool[1].append(result)
|
||||||
|
|
||||||
|
def merge_identify_results(result_map, log):
|
||||||
|
for plugin, results in result_map.iteritems():
|
||||||
|
for result in results:
|
||||||
|
isbn = result.isbn
|
||||||
|
if isbn:
|
||||||
|
isbns, min_year = xisbn.get_isbn_pool(isbn)
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,14 +71,28 @@ class xISBN(object):
|
|||||||
ans.add(i)
|
ans.add(i)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def get_isbn_pool(self, isbn):
|
||||||
|
data = self.get_data(isbn)
|
||||||
|
isbns = frozenset([x.get('isbn') for x in data if 'isbn' in x])
|
||||||
|
min_year = 100000
|
||||||
|
for x in data:
|
||||||
|
try:
|
||||||
|
year = int(x['year'])
|
||||||
|
if year < min_year:
|
||||||
|
min_year = year
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if min_year == 100000:
|
||||||
|
min_year = None
|
||||||
|
return isbns, min_year
|
||||||
|
|
||||||
|
|
||||||
xisbn = xISBN()
|
xisbn = xISBN()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys, pprint
|
||||||
isbn = sys.argv[-1]
|
isbn = sys.argv[-1]
|
||||||
print xisbn.get_data(isbn)
|
print pprint.pprint(xisbn.get_data(isbn))
|
||||||
print
|
print
|
||||||
print xisbn.get_associated_isbns(isbn)
|
print xisbn.get_associated_isbns(isbn)
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@ class MobiMLizer(object):
|
|||||||
def __call__(self, oeb, context):
|
def __call__(self, oeb, context):
|
||||||
oeb.logger.info('Converting XHTML to Mobipocket markup...')
|
oeb.logger.info('Converting XHTML to Mobipocket markup...')
|
||||||
self.oeb = oeb
|
self.oeb = oeb
|
||||||
|
self.log = self.oeb.logger
|
||||||
self.opts = context
|
self.opts = context
|
||||||
self.profile = profile = context.dest
|
self.profile = profile = context.dest
|
||||||
self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items())
|
self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items())
|
||||||
@ -118,6 +119,10 @@ class MobiMLizer(object):
|
|||||||
del oeb.guide['cover']
|
del oeb.guide['cover']
|
||||||
item = oeb.manifest.hrefs[href]
|
item = oeb.manifest.hrefs[href]
|
||||||
if item.spine_position is not None:
|
if item.spine_position is not None:
|
||||||
|
self.log.warn('Found an HTML cover,', item.href, 'removing it.',
|
||||||
|
'If you find some content missing from the output MOBI, it '
|
||||||
|
'is because you misidentified the HTML cover in the input '
|
||||||
|
'document')
|
||||||
oeb.spine.remove(item)
|
oeb.spine.remove(item)
|
||||||
if item.media_type in OEB_DOCS:
|
if item.media_type in OEB_DOCS:
|
||||||
self.oeb.manifest.remove(item)
|
self.oeb.manifest.remove(item)
|
||||||
@ -206,7 +211,11 @@ class MobiMLizer(object):
|
|||||||
vspace = bstate.vpadding + bstate.vmargin
|
vspace = bstate.vpadding + bstate.vmargin
|
||||||
bstate.vpadding = bstate.vmargin = 0
|
bstate.vpadding = bstate.vmargin = 0
|
||||||
if tag not in TABLE_TAGS:
|
if tag not in TABLE_TAGS:
|
||||||
wrapper.attrib['height'] = self.mobimlize_measure(vspace)
|
if tag in ('ul', 'ol') and vspace > 0:
|
||||||
|
wrapper.addprevious(etree.Element(XHTML('div'),
|
||||||
|
height=self.mobimlize_measure(vspace)))
|
||||||
|
else:
|
||||||
|
wrapper.attrib['height'] = self.mobimlize_measure(vspace)
|
||||||
para.attrib['width'] = self.mobimlize_measure(indent)
|
para.attrib['width'] = self.mobimlize_measure(indent)
|
||||||
elif tag == 'table' and vspace > 0:
|
elif tag == 'table' and vspace > 0:
|
||||||
vspace = int(round(vspace / self.profile.fbase))
|
vspace = int(round(vspace / self.profile.fbase))
|
||||||
|
@ -103,8 +103,8 @@ class OEBReader(object):
|
|||||||
data = self.oeb.container.read(None)
|
data = self.oeb.container.read(None)
|
||||||
data = self.oeb.decode(data)
|
data = self.oeb.decode(data)
|
||||||
data = XMLDECL_RE.sub('', data)
|
data = XMLDECL_RE.sub('', data)
|
||||||
data = data.replace('http://openebook.org/namespaces/oeb-package/1.0',
|
data = re.sub(r'http://openebook.org/namespaces/oeb-package/1.0(/*)',
|
||||||
OPF1_NS)
|
OPF1_NS, data)
|
||||||
try:
|
try:
|
||||||
opf = etree.fromstring(data)
|
opf = etree.fromstring(data)
|
||||||
except etree.XMLSyntaxError:
|
except etree.XMLSyntaxError:
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveFakeMargins(object):
|
|
||||||
'''
|
|
||||||
Try to detect and remove fake margins inserted by asinine ebook creation
|
|
||||||
software on each paragraph/wrapper div. Can be used only after CSS
|
|
||||||
flattening.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __call__(self, oeb, opts, log):
|
|
||||||
self.oeb, self.opts, self.log = oeb, opts, log
|
|
||||||
|
|
||||||
from calibre.ebooks.oeb.base import XPath, OEB_STYLES
|
|
||||||
|
|
||||||
stylesheet = None
|
|
||||||
for item in self.oeb.manifest:
|
|
||||||
if item.media_type.lower() in OEB_STYLES:
|
|
||||||
stylesheet = item.data
|
|
||||||
break
|
|
||||||
|
|
||||||
if stylesheet is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
top_level_elements = {}
|
|
||||||
second_level_elements = {}
|
|
||||||
|
|
||||||
for x in self.oeb.spine:
|
|
||||||
root = x.data
|
|
||||||
body = XPath('//h:body')(root)
|
|
||||||
if body:
|
|
||||||
body = body[0]
|
|
||||||
|
|
||||||
if not hasattr(body, 'xpath'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check for margins on top level elements
|
|
||||||
for lb in XPath('./h:div|./h:p|./*/h:div|./*/h:p')(body):
|
|
||||||
cls = lb.get('class', '')
|
|
||||||
level = top_level_elements if lb.getparent() is body else \
|
|
||||||
second_level_elements
|
|
||||||
if cls not in level:
|
|
||||||
level[cls] = []
|
|
||||||
top_level_elements[cls] = []
|
|
||||||
level[cls].append(lb)
|
|
||||||
|
|
||||||
|
|
||||||
def get_margins(self, stylesheet, cls):
|
|
||||||
pass
|
|
||||||
|
|
@ -11,6 +11,26 @@ from collections import Counter
|
|||||||
|
|
||||||
from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath
|
from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath
|
||||||
|
|
||||||
|
class RemoveAdobeMargins(object):
|
||||||
|
'''
|
||||||
|
Remove margins specified in Adobe's page templates.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __call__(self, oeb, log, opts):
|
||||||
|
self.oeb, self.opts, self.log = oeb, opts, log
|
||||||
|
|
||||||
|
for item in self.oeb.manifest:
|
||||||
|
if item.media_type == 'application/vnd.adobe-page-template+xml':
|
||||||
|
self.log('Removing page margins specified in the'
|
||||||
|
' Adobe page template')
|
||||||
|
for elem in item.data.xpath(
|
||||||
|
'//*[@margin-bottom or @margin-top '
|
||||||
|
'or @margin-left or @margin-right]'):
|
||||||
|
for margin in ('left', 'right', 'top', 'bottom'):
|
||||||
|
attr = 'margin-'+margin
|
||||||
|
elem.attrib.pop(attr, None)
|
||||||
|
|
||||||
|
|
||||||
class RemoveFakeMargins(object):
|
class RemoveFakeMargins(object):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -34,7 +34,7 @@ class PDFInput(InputFormatPlugin):
|
|||||||
from calibre.ebooks.pdf.reflow import PDFDocument
|
from calibre.ebooks.pdf.reflow import PDFDocument
|
||||||
if pdfreflow_err:
|
if pdfreflow_err:
|
||||||
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
|
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
|
||||||
pdfreflow.reflow(stream.read())
|
pdfreflow.reflow(stream.read(), 1, -1)
|
||||||
xml = open('index.xml', 'rb').read()
|
xml = open('index.xml', 'rb').read()
|
||||||
PDFDocument(xml, self.opts, self.log)
|
PDFDocument(xml, self.opts, self.log)
|
||||||
return os.path.join(os.getcwd(), 'metadata.opf')
|
return os.path.join(os.getcwd(), 'metadata.opf')
|
||||||
|
@ -24,13 +24,14 @@ extern "C" {
|
|||||||
pdfreflow_reflow(PyObject *self, PyObject *args) {
|
pdfreflow_reflow(PyObject *self, PyObject *args) {
|
||||||
char *pdfdata;
|
char *pdfdata;
|
||||||
Py_ssize_t size;
|
Py_ssize_t size;
|
||||||
|
int first_page, last_page, num = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "s#", &pdfdata, &size))
|
if (!PyArg_ParseTuple(args, "s#ii", &pdfdata, &size, &first_page, &last_page))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Reflow reflow(pdfdata, static_cast<std::ifstream::pos_type>(size));
|
Reflow reflow(pdfdata, static_cast<std::ifstream::pos_type>(size));
|
||||||
reflow.render();
|
num = reflow.render(first_page, last_page);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
PyErr_SetString(PyExc_RuntimeError, e.what()); return NULL;
|
PyErr_SetString(PyExc_RuntimeError, e.what()); return NULL;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
@ -38,7 +39,7 @@ extern "C" {
|
|||||||
"Unknown exception raised while rendering PDF"); return NULL;
|
"Unknown exception raised while rendering PDF"); return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
return Py_BuildValue("i", num);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
@ -166,8 +167,8 @@ extern "C" {
|
|||||||
static
|
static
|
||||||
PyMethodDef pdfreflow_methods[] = {
|
PyMethodDef pdfreflow_methods[] = {
|
||||||
{"reflow", pdfreflow_reflow, METH_VARARGS,
|
{"reflow", pdfreflow_reflow, METH_VARARGS,
|
||||||
"reflow(pdf_data)\n\n"
|
"reflow(pdf_data, first_page, last_page)\n\n"
|
||||||
"Reflow the specified PDF."
|
"Reflow the specified PDF. Returns the number of pages in the PDF. If last_page is -1 renders to end of document."
|
||||||
},
|
},
|
||||||
{"get_metadata", pdfreflow_get_metadata, METH_VARARGS,
|
{"get_metadata", pdfreflow_get_metadata, METH_VARARGS,
|
||||||
"get_metadata(pdf_data, cover)\n\n"
|
"get_metadata(pdf_data, cover)\n\n"
|
||||||
|
@ -712,16 +712,18 @@ Reflow::Reflow(char *pdfdata, size_t sz) :
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
int
|
||||||
Reflow::render() {
|
Reflow::render(int first_page, int last_page) {
|
||||||
|
|
||||||
if (!this->doc->okToCopy())
|
if (!this->doc->okToCopy())
|
||||||
cout << "Warning, this document has the copy protection flag set, ignoring." << endl;
|
cout << "Warning, this document has the copy protection flag set, ignoring." << endl;
|
||||||
|
|
||||||
globalParams->setTextEncoding(encoding);
|
globalParams->setTextEncoding(encoding);
|
||||||
|
|
||||||
int first_page = 1;
|
int doc_pages = doc->getNumPages();
|
||||||
int last_page = doc->getNumPages();
|
if (last_page < 1 or last_page > doc_pages) last_page = doc_pages;
|
||||||
|
if (first_page < 1) first_page = 1;
|
||||||
|
if (first_page > last_page) first_page = last_page;
|
||||||
|
|
||||||
XMLOutputDev *xml_out = new XMLOutputDev(this->doc);
|
XMLOutputDev *xml_out = new XMLOutputDev(this->doc);
|
||||||
doc->displayPages(xml_out, first_page, last_page,
|
doc->displayPages(xml_out, first_page, last_page,
|
||||||
@ -733,9 +735,12 @@ Reflow::render() {
|
|||||||
false //Printing
|
false //Printing
|
||||||
);
|
);
|
||||||
|
|
||||||
this->dump_outline();
|
if (last_page - first_page == doc_pages - 1)
|
||||||
|
this->dump_outline();
|
||||||
|
|
||||||
delete xml_out;
|
delete xml_out;
|
||||||
|
|
||||||
|
return doc_pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reflow::dump_outline() {
|
void Reflow::dump_outline() {
|
||||||
|
@ -66,7 +66,7 @@ class Reflow {
|
|||||||
~Reflow();
|
~Reflow();
|
||||||
|
|
||||||
/* Convert the PDF to XML. All files are output to the current directory */
|
/* Convert the PDF to XML. All files are output to the current directory */
|
||||||
void render();
|
int render(int first_page, int last_page);
|
||||||
|
|
||||||
/* Get the PDF Info Dictionary */
|
/* Get the PDF Info Dictionary */
|
||||||
map<string, string> get_info();
|
map<string, string> get_info();
|
||||||
|
@ -51,7 +51,7 @@ class ConvertAction(InterfaceAction):
|
|||||||
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
self.book_auto_converted, extra_job_args=[on_card])
|
self.book_auto_converted, extra_job_args=[on_card])
|
||||||
|
|
||||||
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format):
|
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format, subject):
|
||||||
previous = self.gui.library_view.currentIndex()
|
previous = self.gui.library_view.currentIndex()
|
||||||
rows = [x.row() for x in \
|
rows = [x.row() for x in \
|
||||||
self.gui.library_view.selectionModel().selectedRows()]
|
self.gui.library_view.selectionModel().selectedRows()]
|
||||||
@ -59,7 +59,7 @@ class ConvertAction(InterfaceAction):
|
|||||||
if jobs == []: return
|
if jobs == []: return
|
||||||
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
|
||||||
self.book_auto_converted_mail,
|
self.book_auto_converted_mail,
|
||||||
extra_job_args=[delete_from_library, to, fmts])
|
extra_job_args=[delete_from_library, to, fmts, subject])
|
||||||
|
|
||||||
def auto_convert_news(self, book_ids, format):
|
def auto_convert_news(self, book_ids, format):
|
||||||
previous = self.gui.library_view.currentIndex()
|
previous = self.gui.library_view.currentIndex()
|
||||||
@ -145,9 +145,10 @@ class ConvertAction(InterfaceAction):
|
|||||||
self.gui.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
self.gui.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
||||||
|
|
||||||
def book_auto_converted_mail(self, job):
|
def book_auto_converted_mail(self, job):
|
||||||
temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job]
|
temp_files, fmt, book_id, delete_from_library, to, fmts, subject = self.conversion_jobs[job]
|
||||||
self.book_converted(job)
|
self.book_converted(job)
|
||||||
self.gui.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
self.gui.send_by_mail(to, fmts, delete_from_library, subject=subject,
|
||||||
|
specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
|
||||||
|
|
||||||
def book_auto_converted_news(self, job):
|
def book_auto_converted_news(self, job):
|
||||||
temp_files, fmt, book_id = self.conversion_jobs[job]
|
temp_files, fmt, book_id = self.conversion_jobs[job]
|
||||||
|
@ -82,7 +82,8 @@ class ShareConnMenu(QMenu): # {{{
|
|||||||
keys = sorted(opts.accounts.keys())
|
keys = sorted(opts.accounts.keys())
|
||||||
for account in keys:
|
for account in keys:
|
||||||
formats, auto, default = opts.accounts[account]
|
formats, auto, default = opts.accounts[account]
|
||||||
dest = 'mail:'+account+';'+formats
|
subject = opts.subjects.get(account, '')
|
||||||
|
dest = 'mail:'+account+';'+formats+';'+subject
|
||||||
action1 = DeviceAction(dest, False, False, I('mail.png'),
|
action1 = DeviceAction(dest, False, False, I('mail.png'),
|
||||||
account)
|
account)
|
||||||
action2 = DeviceAction(dest, True, False, I('mail.png'),
|
action2 = DeviceAction(dest, True, False, I('mail.png'),
|
||||||
|
@ -5,6 +5,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import gc
|
||||||
|
|
||||||
from PyQt4.Qt import Qt
|
from PyQt4.Qt import Qt
|
||||||
|
|
||||||
from calibre.gui2 import Dispatcher
|
from calibre.gui2 import Dispatcher
|
||||||
@ -53,11 +55,11 @@ class FetchNewsAction(InterfaceAction):
|
|||||||
|
|
||||||
def scheduled_recipe_fetched(self, job):
|
def scheduled_recipe_fetched(self, job):
|
||||||
temp_files, fmt, arg = self.conversion_jobs.pop(job)
|
temp_files, fmt, arg = self.conversion_jobs.pop(job)
|
||||||
pt = temp_files[0]
|
fname = temp_files[0].name
|
||||||
if job.failed:
|
if job.failed:
|
||||||
self.scheduler.recipe_download_failed(arg)
|
self.scheduler.recipe_download_failed(arg)
|
||||||
return self.gui.job_exception(job)
|
return self.gui.job_exception(job)
|
||||||
id = self.gui.library_view.model().add_news(pt.name, arg)
|
id = self.gui.library_view.model().add_news(fname, arg)
|
||||||
|
|
||||||
# Arg may contain a "keep_issues" variable. If it is non-zero,
|
# Arg may contain a "keep_issues" variable. If it is non-zero,
|
||||||
# delete all but newest x issues.
|
# delete all but newest x issues.
|
||||||
@ -81,5 +83,6 @@ class FetchNewsAction(InterfaceAction):
|
|||||||
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
||||||
self.gui.email_news(id)
|
self.gui.email_news(id)
|
||||||
self.gui.sync_news()
|
self.gui.sync_news()
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ class DeviceManager(Thread): # {{{
|
|||||||
dev.open(self.current_library_uuid)
|
dev.open(self.current_library_uuid)
|
||||||
except OpenFeedback as e:
|
except OpenFeedback as e:
|
||||||
if dev not in self.ejected_devices:
|
if dev not in self.ejected_devices:
|
||||||
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
|
self.open_feedback_msg(dev.get_gui_name(), e)
|
||||||
self.ejected_devices.add(dev)
|
self.ejected_devices.add(dev)
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
@ -618,8 +618,11 @@ class DeviceMixin(object): # {{{
|
|||||||
if tweaks['auto_connect_to_folder']:
|
if tweaks['auto_connect_to_folder']:
|
||||||
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
|
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
|
||||||
|
|
||||||
def show_open_feedback(self, devname, msg):
|
def show_open_feedback(self, devname, e):
|
||||||
self.__of_dev_mem__ = d = info_dialog(self, devname, msg)
|
try:
|
||||||
|
self.__of_dev_mem__ = d = e.custom_dialog(self)
|
||||||
|
except NotImplementedError:
|
||||||
|
self.__of_dev_mem__ = d = info_dialog(self, devname, e.feedback_msg)
|
||||||
d.show()
|
d.show()
|
||||||
|
|
||||||
def auto_convert_question(self, msg, autos):
|
def auto_convert_question(self, msg, autos):
|
||||||
@ -884,9 +887,14 @@ class DeviceMixin(object): # {{{
|
|||||||
on_card = dest
|
on_card = dest
|
||||||
self.sync_to_device(on_card, delete, fmt)
|
self.sync_to_device(on_card, delete, fmt)
|
||||||
elif dest == 'mail':
|
elif dest == 'mail':
|
||||||
to, fmts = sub_dest.split(';')
|
sub_dest_parts = sub_dest.split(';')
|
||||||
|
while len(sub_dest_parts) < 3:
|
||||||
|
sub_dest_parts.append('')
|
||||||
|
to = sub_dest_parts[0]
|
||||||
|
fmts = sub_dest_parts[1]
|
||||||
|
subject = ';'.join(sub_dest_parts[2:])
|
||||||
fmts = [x.strip().lower() for x in fmts.split(',')]
|
fmts = [x.strip().lower() for x in fmts.split(',')]
|
||||||
self.send_by_mail(to, fmts, delete)
|
self.send_by_mail(to, fmts, delete, subject=subject)
|
||||||
|
|
||||||
def cover_to_thumbnail(self, data):
|
def cover_to_thumbnail(self, data):
|
||||||
if self.device_manager.device and \
|
if self.device_manager.device and \
|
||||||
|
@ -3,12 +3,11 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from calibre.gui2 import dynamic
|
|
||||||
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
|
|
||||||
from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon
|
from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon
|
||||||
|
|
||||||
def _config_name(name):
|
from calibre import confirm_config_name
|
||||||
return name + '_again'
|
from calibre.gui2 import dynamic
|
||||||
|
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
|
||||||
|
|
||||||
class Dialog(QDialog, Ui_Dialog):
|
class Dialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
@ -22,11 +21,11 @@ class Dialog(QDialog, Ui_Dialog):
|
|||||||
self.buttonBox.setFocus(Qt.OtherFocusReason)
|
self.buttonBox.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
def toggle(self, *args):
|
def toggle(self, *args):
|
||||||
dynamic[_config_name(self.name)] = self.again.isChecked()
|
dynamic[confirm_config_name(self.name)] = self.again.isChecked()
|
||||||
|
|
||||||
|
|
||||||
def confirm(msg, name, parent=None, pixmap='dialog_warning.png'):
|
def confirm(msg, name, parent=None, pixmap='dialog_warning.png'):
|
||||||
if not dynamic.get(_config_name(name), True):
|
if not dynamic.get(confirm_config_name(name), True):
|
||||||
return True
|
return True
|
||||||
d = Dialog(msg, name, parent)
|
d = Dialog(msg, name, parent)
|
||||||
d.label.setPixmap(QPixmap(I(pixmap)))
|
d.label.setPixmap(QPixmap(I(pixmap)))
|
||||||
|
@ -22,6 +22,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma
|
|||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.gui2 import config, Dispatcher, warning_dialog
|
from calibre.gui2 import config, Dispatcher, warning_dialog
|
||||||
|
from calibre.library.save_to_disk import get_components
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
|
|
||||||
class EmailJob(BaseJob): # {{{
|
class EmailJob(BaseJob): # {{{
|
||||||
@ -210,7 +211,7 @@ class EmailMixin(object): # {{{
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.emailer = Emailer(self.job_manager)
|
self.emailer = Emailer(self.job_manager)
|
||||||
|
|
||||||
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None,
|
def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None,
|
||||||
do_auto_convert=True, specific_format=None):
|
do_auto_convert=True, specific_format=None):
|
||||||
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
||||||
if not ids or len(ids) == 0:
|
if not ids or len(ids) == 0:
|
||||||
@ -239,7 +240,14 @@ class EmailMixin(object): # {{{
|
|||||||
remove_ids.append(id)
|
remove_ids.append(id)
|
||||||
jobnames.append(t)
|
jobnames.append(t)
|
||||||
attachments.append(f)
|
attachments.append(f)
|
||||||
subjects.append(_('E-book:')+ ' '+t)
|
if not subject:
|
||||||
|
subjects.append(_('E-book:')+ ' '+t)
|
||||||
|
else:
|
||||||
|
components = get_components(subject, mi, id)
|
||||||
|
if not components:
|
||||||
|
components = [mi.title]
|
||||||
|
subject = os.path.join(*components)
|
||||||
|
subjects.append(subject)
|
||||||
a = authors_to_string(mi.authors if mi.authors else \
|
a = authors_to_string(mi.authors if mi.authors else \
|
||||||
[_('Unknown')])
|
[_('Unknown')])
|
||||||
texts.append(_('Attached, you will find the e-book') + \
|
texts.append(_('Attached, you will find the e-book') + \
|
||||||
@ -292,7 +300,7 @@ class EmailMixin(object): # {{{
|
|||||||
if self.auto_convert_question(
|
if self.auto_convert_question(
|
||||||
_('Auto convert the following books before sending via '
|
_('Auto convert the following books before sending via '
|
||||||
'email?'), autos):
|
'email?'), autos):
|
||||||
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format)
|
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format, subject)
|
||||||
|
|
||||||
if bad:
|
if bad:
|
||||||
bad = '\n'.join('%s'%(i,) for i in bad)
|
bad = '\n'.join('%s'%(i,) for i in bad)
|
||||||
|
@ -206,6 +206,46 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>Publisher:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<widget class="QLineEdit" name="publisher">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Regular expression (?P<publisher>)</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>No match</string>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="text">
|
||||||
|
<string>Published:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QLineEdit" name="pubdate">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Regular expression (?P<published>)</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>No match</string>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import shutil, functools, re, os, traceback
|
import shutil, functools, re, os, traceback
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from operator import attrgetter
|
|
||||||
|
|
||||||
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
|
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
|
||||||
QModelIndex, QVariant, QDate, QColor
|
QModelIndex, QVariant, QDate, QColor
|
||||||
@ -18,7 +17,7 @@ from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_autho
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import tweaks, prefs
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
||||||
from calibre.utils.icu import sort_key, strcmp as icu_strcmp
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
@ -984,6 +983,21 @@ class OnDeviceSearch(SearchQueryParser): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
class DeviceDBSortKeyGen(object): # {{{
|
||||||
|
|
||||||
|
def __init__(self, attr, keyfunc, db):
|
||||||
|
self.attr = attr
|
||||||
|
self.db = db
|
||||||
|
self.keyfunc = keyfunc
|
||||||
|
|
||||||
|
def __call__(self, x):
|
||||||
|
try:
|
||||||
|
ans = self.keyfunc(getattr(self.db[x], self.attr))
|
||||||
|
except:
|
||||||
|
ans = None
|
||||||
|
return ans
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceBooksModel(BooksModel): # {{{
|
class DeviceBooksModel(BooksModel): # {{{
|
||||||
|
|
||||||
booklist_dirtied = pyqtSignal()
|
booklist_dirtied = pyqtSignal()
|
||||||
@ -1089,59 +1103,40 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
descending = order != Qt.AscendingOrder
|
descending = order != Qt.AscendingOrder
|
||||||
def strcmp(attr):
|
|
||||||
ag = attrgetter(attr)
|
|
||||||
def _strcmp(x, y):
|
|
||||||
x = ag(self.db[x])
|
|
||||||
y = ag(self.db[y])
|
|
||||||
if x == None:
|
|
||||||
x = ''
|
|
||||||
if y == None:
|
|
||||||
y = ''
|
|
||||||
return icu_strcmp(x.strip(), y.strip())
|
|
||||||
return _strcmp
|
|
||||||
def datecmp(x, y):
|
|
||||||
x = self.db[x].datetime
|
|
||||||
y = self.db[y].datetime
|
|
||||||
return cmp(dt_factory(x, assume_utc=True), dt_factory(y,
|
|
||||||
assume_utc=True))
|
|
||||||
def sizecmp(x, y):
|
|
||||||
x, y = int(self.db[x].size), int(self.db[y].size)
|
|
||||||
return cmp(x, y)
|
|
||||||
def tagscmp(x, y):
|
|
||||||
x = ','.join(sorted(getattr(self.db[x], 'device_collections', []),key=sort_key))
|
|
||||||
y = ','.join(sorted(getattr(self.db[y], 'device_collections', []),key=sort_key))
|
|
||||||
return cmp(x, y)
|
|
||||||
def libcmp(x, y):
|
|
||||||
x, y = self.db[x].in_library, self.db[y].in_library
|
|
||||||
return cmp(x, y)
|
|
||||||
def authorcmp(x, y):
|
|
||||||
ax = getattr(self.db[x], 'author_sort', None)
|
|
||||||
ay = getattr(self.db[y], 'author_sort', None)
|
|
||||||
if ax and ay:
|
|
||||||
x = ax
|
|
||||||
y = ay
|
|
||||||
else:
|
|
||||||
x, y = authors_to_string(self.db[x].authors), \
|
|
||||||
authors_to_string(self.db[y].authors)
|
|
||||||
return cmp(x, y)
|
|
||||||
cname = self.column_map[col]
|
cname = self.column_map[col]
|
||||||
fcmp = {
|
def author_key(x):
|
||||||
'title': strcmp('title_sorter'),
|
try:
|
||||||
'authors' : authorcmp,
|
ax = self.db[x].author_sort
|
||||||
'size' : sizecmp,
|
if not ax:
|
||||||
'timestamp': datecmp,
|
raise Exception('')
|
||||||
'collections': tagscmp,
|
except:
|
||||||
'inlibrary': libcmp,
|
try:
|
||||||
|
ax = authors_to_string(self.db[x].authors)
|
||||||
|
except:
|
||||||
|
ax = ''
|
||||||
|
return ax
|
||||||
|
|
||||||
|
keygen = {
|
||||||
|
'title': ('title_sorter', lambda x: sort_key(x) if x else ''),
|
||||||
|
'authors' : author_key,
|
||||||
|
'size' : ('size', int),
|
||||||
|
'timestamp': ('datetime', functools.partial(dt_factory, assume_utc=True)),
|
||||||
|
'collections': ('device_collections', lambda x:sorted(x,
|
||||||
|
key=sort_key)),
|
||||||
|
'inlibrary': ('in_library', lambda x: x),
|
||||||
}[cname]
|
}[cname]
|
||||||
self.map.sort(cmp=fcmp, reverse=descending)
|
keygen = keygen if callable(keygen) else DeviceDBSortKeyGen(
|
||||||
|
keygen[0], keygen[1], self.db)
|
||||||
|
self.map.sort(key=keygen, reverse=descending)
|
||||||
if len(self.map) == len(self.db):
|
if len(self.map) == len(self.db):
|
||||||
self.sorted_map = list(self.map)
|
self.sorted_map = list(self.map)
|
||||||
else:
|
else:
|
||||||
self.sorted_map = list(range(len(self.db)))
|
self.sorted_map = list(range(len(self.db)))
|
||||||
self.sorted_map.sort(cmp=fcmp, reverse=descending)
|
self.sorted_map.sort(key=keygen, reverse=descending)
|
||||||
self.sorted_on = (self.column_map[col], order)
|
self.sorted_on = (self.column_map[col], order)
|
||||||
self.sort_history.insert(0, self.sorted_on)
|
self.sort_history.insert(0, self.sorted_on)
|
||||||
|
if hasattr(keygen, 'db'):
|
||||||
|
keygen.db = None
|
||||||
if reset:
|
if reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt
|
from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt
|
||||||
|
|
||||||
|
|
||||||
@ -17,25 +19,30 @@ from calibre.utils.smtp import config as smtp_prefs
|
|||||||
|
|
||||||
class EmailAccounts(QAbstractTableModel): # {{{
|
class EmailAccounts(QAbstractTableModel): # {{{
|
||||||
|
|
||||||
def __init__(self, accounts):
|
def __init__(self, accounts, subjects):
|
||||||
QAbstractTableModel.__init__(self)
|
QAbstractTableModel.__init__(self)
|
||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
|
self.subjects = subjects
|
||||||
self.account_order = sorted(self.accounts.keys())
|
self.account_order = sorted(self.accounts.keys())
|
||||||
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')])
|
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Subject'), _('Auto send')])
|
||||||
self.default_font = QFont()
|
self.default_font = QFont()
|
||||||
self.default_font.setBold(True)
|
self.default_font.setBold(True)
|
||||||
self.default_font = QVariant(self.default_font)
|
self.default_font = QVariant(self.default_font)
|
||||||
self.tooltips =[NONE] + map(QVariant,
|
self.tooltips =[NONE] + list(map(QVariant, map(textwrap.fill,
|
||||||
[_('Formats to email. The first matching format will be sent.'),
|
[_('Formats to email. The first matching format will be sent.'),
|
||||||
|
_('Subject of the email to use when sending. When left blank '
|
||||||
|
'the title will be used for the subject. Also, the same '
|
||||||
|
'templates used for "Save to disk" such as {title} and '
|
||||||
|
'{author_sort} can be used here.'),
|
||||||
'<p>'+_('If checked, downloaded news will be automatically '
|
'<p>'+_('If checked, downloaded news will be automatically '
|
||||||
'mailed <br>to this email address '
|
'mailed <br>to this email address '
|
||||||
'(provided it is in one of the listed formats).')])
|
'(provided it is in one of the listed formats).')])))
|
||||||
|
|
||||||
def rowCount(self, *args):
|
def rowCount(self, *args):
|
||||||
return len(self.account_order)
|
return len(self.account_order)
|
||||||
|
|
||||||
def columnCount(self, *args):
|
def columnCount(self, *args):
|
||||||
return 3
|
return len(self.headers)
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||||
@ -56,14 +63,16 @@ class EmailAccounts(QAbstractTableModel): # {{{
|
|||||||
return QVariant(account)
|
return QVariant(account)
|
||||||
if col == 1:
|
if col == 1:
|
||||||
return QVariant(self.accounts[account][0])
|
return QVariant(self.accounts[account][0])
|
||||||
|
if col == 2:
|
||||||
|
return QVariant(self.subjects.get(account, ''))
|
||||||
if role == Qt.FontRole and self.accounts[account][2]:
|
if role == Qt.FontRole and self.accounts[account][2]:
|
||||||
return self.default_font
|
return self.default_font
|
||||||
if role == Qt.CheckStateRole and col == 2:
|
if role == Qt.CheckStateRole and col == 3:
|
||||||
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
|
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
if index.column() == 2:
|
if index.column() == 3:
|
||||||
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
|
||||||
else:
|
else:
|
||||||
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
|
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
|
||||||
@ -73,8 +82,10 @@ class EmailAccounts(QAbstractTableModel): # {{{
|
|||||||
return False
|
return False
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
account = self.account_order[row]
|
account = self.account_order[row]
|
||||||
if col == 2:
|
if col == 3:
|
||||||
self.accounts[account][1] ^= True
|
self.accounts[account][1] ^= True
|
||||||
|
if col == 2:
|
||||||
|
self.subjects[account] = unicode(value.toString())
|
||||||
elif col == 1:
|
elif col == 1:
|
||||||
self.accounts[account][0] = unicode(value.toString()).upper()
|
self.accounts[account][0] = unicode(value.toString()).upper()
|
||||||
else:
|
else:
|
||||||
@ -143,7 +154,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.send_email_widget.initialize(self.preferred_to_address)
|
self.send_email_widget.initialize(self.preferred_to_address)
|
||||||
self.send_email_widget.changed_signal.connect(self.changed_signal.emit)
|
self.send_email_widget.changed_signal.connect(self.changed_signal.emit)
|
||||||
opts = self.send_email_widget.smtp_opts
|
opts = self.send_email_widget.smtp_opts
|
||||||
self._email_accounts = EmailAccounts(opts.accounts)
|
self._email_accounts = EmailAccounts(opts.accounts, opts.subjects)
|
||||||
self._email_accounts.dataChanged.connect(lambda x,y:
|
self._email_accounts.dataChanged.connect(lambda x,y:
|
||||||
self.changed_signal.emit())
|
self.changed_signal.emit())
|
||||||
self.email_view.setModel(self._email_accounts)
|
self.email_view.setModel(self._email_accounts)
|
||||||
@ -170,6 +181,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
if not self.send_email_widget.set_email_settings(to_set):
|
if not self.send_email_widget.set_email_settings(to_set):
|
||||||
raise AbortCommit('abort')
|
raise AbortCommit('abort')
|
||||||
self.proxy['accounts'] = self._email_accounts.accounts
|
self.proxy['accounts'] = self._email_accounts.accounts
|
||||||
|
self.proxy['subjects'] = self._email_accounts.subjects
|
||||||
|
|
||||||
return ConfigWidgetBase.commit(self)
|
return ConfigWidgetBase.commit(self)
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ class SearchBox2(QComboBox): # {{{
|
|||||||
def normalize_state(self):
|
def normalize_state(self):
|
||||||
self.setToolTip(self.tool_tip_text)
|
self.setToolTip(self.tool_tip_text)
|
||||||
self.line_edit.setStyleSheet(
|
self.line_edit.setStyleSheet(
|
||||||
'QLineEdit{color:black;background-color:%s;}' % self.normal_background)
|
'QLineEdit{color:inherit;background-color:%s;}' % self.normal_background)
|
||||||
|
|
||||||
def text(self):
|
def text(self):
|
||||||
return self.currentText()
|
return self.currentText()
|
||||||
|
@ -171,10 +171,11 @@ class Document(QWebPage): # {{{
|
|||||||
self.misc_config()
|
self.misc_config()
|
||||||
self.after_load()
|
self.after_load()
|
||||||
|
|
||||||
def __init__(self, shortcuts, parent=None):
|
def __init__(self, shortcuts, parent=None, resize_callback=lambda: None):
|
||||||
QWebPage.__init__(self, parent)
|
QWebPage.__init__(self, parent)
|
||||||
self.setObjectName("py_bridge")
|
self.setObjectName("py_bridge")
|
||||||
self.debug_javascript = False
|
self.debug_javascript = False
|
||||||
|
self.resize_callback = resize_callback
|
||||||
self.current_language = None
|
self.current_language = None
|
||||||
self.loaded_javascript = False
|
self.loaded_javascript = False
|
||||||
|
|
||||||
@ -237,6 +238,12 @@ class Document(QWebPage): # {{{
|
|||||||
if self.loaded_javascript:
|
if self.loaded_javascript:
|
||||||
return
|
return
|
||||||
self.loaded_javascript = True
|
self.loaded_javascript = True
|
||||||
|
self.javascript(
|
||||||
|
'''
|
||||||
|
window.onresize = function(event) {
|
||||||
|
window.py_bridge.window_resized();
|
||||||
|
}
|
||||||
|
''')
|
||||||
if jquery is None:
|
if jquery is None:
|
||||||
jquery = P('content_server/jquery.js', data=True)
|
jquery = P('content_server/jquery.js', data=True)
|
||||||
self.javascript(jquery)
|
self.javascript(jquery)
|
||||||
@ -298,6 +305,10 @@ class Document(QWebPage): # {{{
|
|||||||
def debug(self, msg):
|
def debug(self, msg):
|
||||||
prints(msg)
|
prints(msg)
|
||||||
|
|
||||||
|
@pyqtSignature('')
|
||||||
|
def window_resized(self):
|
||||||
|
self.resize_callback()
|
||||||
|
|
||||||
def reference_mode(self, enable):
|
def reference_mode(self, enable):
|
||||||
self.javascript(('enter' if enable else 'leave')+'_reference_mode()')
|
self.javascript(('enter' if enable else 'leave')+'_reference_mode()')
|
||||||
|
|
||||||
@ -424,12 +435,19 @@ class Document(QWebPage): # {{{
|
|||||||
def xpos(self):
|
def xpos(self):
|
||||||
return self.mainFrame().scrollPosition().x()
|
return self.mainFrame().scrollPosition().x()
|
||||||
|
|
||||||
@property
|
@dynamic_property
|
||||||
def scroll_fraction(self):
|
def scroll_fraction(self):
|
||||||
try:
|
def fget(self):
|
||||||
return float(self.ypos)/(self.height-self.window_height)
|
try:
|
||||||
except ZeroDivisionError:
|
return float(self.ypos)/(self.height-self.window_height)
|
||||||
return 0.
|
except ZeroDivisionError:
|
||||||
|
return 0.
|
||||||
|
def fset(self, val):
|
||||||
|
npos = val * (self.height - self.window_height)
|
||||||
|
if npos < 0:
|
||||||
|
npos = 0
|
||||||
|
self.scroll_to(x=self.xpos, y=npos)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hscroll_fraction(self):
|
def hscroll_fraction(self):
|
||||||
@ -493,7 +511,8 @@ class DocumentView(QWebView): # {{{
|
|||||||
self._size_hint = QSize(510, 680)
|
self._size_hint = QSize(510, 680)
|
||||||
self.initial_pos = 0.0
|
self.initial_pos = 0.0
|
||||||
self.to_bottom = False
|
self.to_bottom = False
|
||||||
self.document = Document(self.shortcuts, parent=self)
|
self.document = Document(self.shortcuts, parent=self,
|
||||||
|
resize_callback=self.viewport_resized)
|
||||||
self.setPage(self.document)
|
self.setPage(self.document)
|
||||||
self.manager = None
|
self.manager = None
|
||||||
self._reference_mode = False
|
self._reference_mode = False
|
||||||
@ -630,9 +649,13 @@ class DocumentView(QWebView): # {{{
|
|||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return self._size_hint
|
return self._size_hint
|
||||||
|
|
||||||
@property
|
@dynamic_property
|
||||||
def scroll_fraction(self):
|
def scroll_fraction(self):
|
||||||
return self.document.scroll_fraction
|
def fget(self):
|
||||||
|
return self.document.scroll_fraction
|
||||||
|
def fset(self, val):
|
||||||
|
self.document.scroll_fraction = float(val)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hscroll_fraction(self):
|
def hscroll_fraction(self):
|
||||||
@ -968,9 +991,11 @@ class DocumentView(QWebView): # {{{
|
|||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
ret = QWebView.resizeEvent(self, event)
|
ret = QWebView.resizeEvent(self, event)
|
||||||
QTimer.singleShot(10, self.initialize_scrollbar)
|
QTimer.singleShot(10, self.initialize_scrollbar)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def viewport_resized(self):
|
||||||
if self.manager is not None:
|
if self.manager is not None:
|
||||||
self.manager.viewport_resized(self.scroll_fraction)
|
self.manager.viewport_resized(self.scroll_fraction)
|
||||||
return ret
|
|
||||||
|
|
||||||
def event(self, ev):
|
def event(self, ev):
|
||||||
typ = ev.type()
|
typ = ev.type()
|
||||||
|
@ -240,7 +240,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'),
|
self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'),
|
||||||
lambda x: self.view.reference_mode(x))
|
lambda x: self.view.reference_mode(x))
|
||||||
self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x))
|
self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x))
|
||||||
self.connect(self.action_table_of_contents, SIGNAL('toggled(bool)'), lambda x:self.toc.setVisible(x))
|
self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible)
|
||||||
self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy)
|
self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy)
|
||||||
self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'),
|
self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'),
|
||||||
self.font_size_larger)
|
self.font_size_larger)
|
||||||
@ -310,6 +310,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
|
def set_toc_visible(self, yes):
|
||||||
|
self.toc.setVisible(yes)
|
||||||
|
|
||||||
def clear_recent_history(self, *args):
|
def clear_recent_history(self, *args):
|
||||||
vprefs.set('viewer_open_history', [])
|
vprefs.set('viewer_open_history', [])
|
||||||
self.build_recent_menu()
|
self.build_recent_menu()
|
||||||
|
@ -121,6 +121,12 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
else:
|
else:
|
||||||
self.series_index.setText(_('No match'))
|
self.series_index.setText(_('No match'))
|
||||||
|
|
||||||
|
if mi.publisher:
|
||||||
|
self.publisher.setText(mi.publisher)
|
||||||
|
|
||||||
|
if mi.pubdate:
|
||||||
|
self.pubdate.setText(mi.pubdate.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))
|
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +76,8 @@ class CustomColumns(object):
|
|||||||
'num':record[6],
|
'num':record[6],
|
||||||
'is_multiple':record[7],
|
'is_multiple':record[7],
|
||||||
}
|
}
|
||||||
|
if data['display'] is None:
|
||||||
|
data['display'] = {}
|
||||||
table, lt = self.custom_table_names(data['num'])
|
table, lt = self.custom_table_names(data['num'])
|
||||||
if table not in custom_tables or (data['normalized'] and lt not in
|
if table not in custom_tables or (data['normalized'] and lt not in
|
||||||
custom_tables):
|
custom_tables):
|
||||||
|
@ -31,8 +31,11 @@ class cmd_commit(_cmd_commit):
|
|||||||
summary = ''
|
summary = ''
|
||||||
raw = urllib.urlopen('https://bugs.launchpad.net/calibre/+bug/' +
|
raw = urllib.urlopen('https://bugs.launchpad.net/calibre/+bug/' +
|
||||||
bug).read()
|
bug).read()
|
||||||
h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0]
|
try:
|
||||||
summary = html.tostring(h1, method='text', encoding=unicode).strip()
|
h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0]
|
||||||
|
summary = html.tostring(h1, method='text', encoding=unicode).strip()
|
||||||
|
except:
|
||||||
|
summary = 'Private bug'
|
||||||
print 'Working on bug:', summary
|
print 'Working on bug:', summary
|
||||||
if summary:
|
if summary:
|
||||||
msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary))
|
msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary))
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""html2text: Turn HTML into equivalent Markdown-structured text."""
|
"""html2text: Turn HTML into equivalent Markdown-structured text."""
|
||||||
__version__ = "2.39"
|
# Last upstream version before changes
|
||||||
__author__ = "Aaron Swartz (me@aaronsw.com)"
|
#__version__ = "2.39"
|
||||||
__copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3."
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '''
|
||||||
|
Copyright (c) 2011, John Schember <john@nachtimwald.com>
|
||||||
|
(C) 2004-2008 Aaron Swartz <me@aaronsw.com>
|
||||||
|
'''
|
||||||
__contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
|
__contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
@ -11,7 +17,6 @@ __contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
|
|||||||
if not hasattr(__builtins__, 'True'): True, False = 1, 0
|
if not hasattr(__builtins__, 'True'): True, False = 1, 0
|
||||||
import re, sys, urllib, htmlentitydefs, codecs
|
import re, sys, urllib, htmlentitydefs, codecs
|
||||||
import sgmllib
|
import sgmllib
|
||||||
import urlparse
|
|
||||||
sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]')
|
sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]')
|
||||||
|
|
||||||
try: from textwrap import wrap
|
try: from textwrap import wrap
|
||||||
@ -145,9 +150,7 @@ class _html2text(sgmllib.SGMLParser):
|
|||||||
self.outcount = 0
|
self.outcount = 0
|
||||||
self.start = 1
|
self.start = 1
|
||||||
self.space = 0
|
self.space = 0
|
||||||
self.a = []
|
|
||||||
self.astack = []
|
self.astack = []
|
||||||
self.acount = 0
|
|
||||||
self.list = []
|
self.list = []
|
||||||
self.blockquote = 0
|
self.blockquote = 0
|
||||||
self.pre = 0
|
self.pre = 0
|
||||||
@ -181,29 +184,6 @@ class _html2text(sgmllib.SGMLParser):
|
|||||||
def unknown_endtag(self, tag):
|
def unknown_endtag(self, tag):
|
||||||
self.handle_tag(tag, None, 0)
|
self.handle_tag(tag, None, 0)
|
||||||
|
|
||||||
def previousIndex(self, attrs):
|
|
||||||
""" returns the index of certain set of attributes (of a link) in the
|
|
||||||
self.a list
|
|
||||||
|
|
||||||
If the set of attributes is not found, returns None
|
|
||||||
"""
|
|
||||||
if not attrs.has_key('href'): return None
|
|
||||||
|
|
||||||
i = -1
|
|
||||||
for a in self.a:
|
|
||||||
i += 1
|
|
||||||
match = 0
|
|
||||||
|
|
||||||
if a.has_key('href') and a['href'] == attrs['href']:
|
|
||||||
if a.has_key('title') or attrs.has_key('title'):
|
|
||||||
if (a.has_key('title') and attrs.has_key('title') and
|
|
||||||
a['title'] == attrs['title']):
|
|
||||||
match = True
|
|
||||||
else:
|
|
||||||
match = True
|
|
||||||
|
|
||||||
if match: return i
|
|
||||||
|
|
||||||
def handle_tag(self, tag, attrs, start):
|
def handle_tag(self, tag, attrs, start):
|
||||||
attrs = fixattrs(attrs)
|
attrs = fixattrs(attrs)
|
||||||
|
|
||||||
@ -268,34 +248,23 @@ class _html2text(sgmllib.SGMLParser):
|
|||||||
if self.astack:
|
if self.astack:
|
||||||
a = self.astack.pop()
|
a = self.astack.pop()
|
||||||
if a:
|
if a:
|
||||||
i = self.previousIndex(a)
|
title = ''
|
||||||
if i is not None:
|
if a.has_key('title'):
|
||||||
a = self.a[i]
|
title = ' "%s"' % a['title']
|
||||||
else:
|
self.o('](%s%s)' % (a['href'], title))
|
||||||
self.acount += 1
|
|
||||||
a['count'] = self.acount
|
|
||||||
a['outcount'] = self.outcount
|
|
||||||
self.a.append(a)
|
|
||||||
self.o("][" + `a['count']` + "]")
|
|
||||||
|
|
||||||
if tag == "img" and start:
|
if tag == "img" and start:
|
||||||
attrsD = {}
|
attrsD = {}
|
||||||
for (x, y) in attrs: attrsD[x] = y
|
for (x, y) in attrs: attrsD[x] = y
|
||||||
attrs = attrsD
|
attrs = attrsD
|
||||||
if attrs.has_key('src'):
|
if attrs.has_key('src'):
|
||||||
attrs['href'] = attrs['src']
|
|
||||||
alt = attrs.get('alt', '')
|
alt = attrs.get('alt', '')
|
||||||
i = self.previousIndex(attrs)
|
|
||||||
if i is not None:
|
|
||||||
attrs = self.a[i]
|
|
||||||
else:
|
|
||||||
self.acount += 1
|
|
||||||
attrs['count'] = self.acount
|
|
||||||
attrs['outcount'] = self.outcount
|
|
||||||
self.a.append(attrs)
|
|
||||||
self.o("![")
|
self.o("![")
|
||||||
self.o(alt)
|
self.o(alt)
|
||||||
self.o("]["+`attrs['count']`+"]")
|
title = ''
|
||||||
|
if attrs.has_key('title'):
|
||||||
|
title = ' "%s"' % attrs['title']
|
||||||
|
self.o('](%s%s)' % (attrs['src'], title))
|
||||||
|
|
||||||
if tag == 'dl' and start: self.p()
|
if tag == 'dl' and start: self.p()
|
||||||
if tag == 'dt' and not start: self.pbr()
|
if tag == 'dt' and not start: self.pbr()
|
||||||
@ -373,7 +342,6 @@ class _html2text(sgmllib.SGMLParser):
|
|||||||
self.out("\n")
|
self.out("\n")
|
||||||
self.space = 0
|
self.space = 0
|
||||||
|
|
||||||
|
|
||||||
if self.p_p:
|
if self.p_p:
|
||||||
self.out(('\n'+bq)*self.p_p)
|
self.out(('\n'+bq)*self.p_p)
|
||||||
self.space = 0
|
self.space = 0
|
||||||
@ -382,22 +350,6 @@ class _html2text(sgmllib.SGMLParser):
|
|||||||
if not self.lastWasNL: self.out(' ')
|
if not self.lastWasNL: self.out(' ')
|
||||||
self.space = 0
|
self.space = 0
|
||||||
|
|
||||||
if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"):
|
|
||||||
if force == "end": self.out("\n")
|
|
||||||
|
|
||||||
newa = []
|
|
||||||
for link in self.a:
|
|
||||||
if self.outcount > link['outcount']:
|
|
||||||
self.out(" ["+`link['count']`+"]: " + urlparse.urljoin(self.baseurl, link['href']))
|
|
||||||
if link.has_key('title'): self.out(" ("+link['title']+")")
|
|
||||||
self.out("\n")
|
|
||||||
else:
|
|
||||||
newa.append(link)
|
|
||||||
|
|
||||||
if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done.
|
|
||||||
|
|
||||||
self.a = newa
|
|
||||||
|
|
||||||
if self.abbr_list and force == "end":
|
if self.abbr_list and force == "end":
|
||||||
for abbr, definition in self.abbr_list.items():
|
for abbr, definition in self.abbr_list.items():
|
||||||
self.out(" *[" + abbr + "]: " + definition + "\n")
|
self.out(" *[" + abbr + "]: " + definition + "\n")
|
||||||
|
@ -250,6 +250,7 @@ def config(defaults=None):
|
|||||||
c = Config('smtp',desc) if defaults is None else StringConfig(defaults,desc)
|
c = Config('smtp',desc) if defaults is None else StringConfig(defaults,desc)
|
||||||
c.add_opt('from_')
|
c.add_opt('from_')
|
||||||
c.add_opt('accounts', default={})
|
c.add_opt('accounts', default={})
|
||||||
|
c.add_opt('subjects', default={})
|
||||||
c.add_opt('relay_host')
|
c.add_opt('relay_host')
|
||||||
c.add_opt('relay_port', default=25)
|
c.add_opt('relay_port', default=25)
|
||||||
c.add_opt('relay_username')
|
c.add_opt('relay_username')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user