merge from trunk

This commit is contained in:
Lee 2011-02-28 07:57:56 +08:00
commit b0d45bab5f
20 changed files with 803 additions and 454 deletions

View File

@ -92,8 +92,8 @@
- title: "Various Romanian news sources"
author: Silviu Coatara
- title: "Osnews.pl and SwiatKindle"
author: Mori
- title: "Osnews.pl and SwiatCzytnikow"
author: Tomasz Dlugosz
- title: "Roger Ebert Journal"
author: Shane Erstad

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
bucataras.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Bucataras(BasicNewsRecipe):
title = u'Bucataras'
__author__ = u'Silviu Cotoar\u0103'
description = ''
publisher = 'Bucataras'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Bucatarie,Retete'
encoding = 'utf-8'
cover_url = 'http://www.bucataras.ro/templates/default/images/pink/logo.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='h1', attrs={'class':'titlu'})
, dict(name='div', attrs={'class':'contentL'})
, dict(name='div', attrs={'class':'contentBottom'})
]
remove_tags = [
dict(name='div', attrs={'class':['sociale']})
, dict(name='div', attrs={'class':['contentR']})
, dict(name='a', attrs={'target':['_self']})
, dict(name='div', attrs={'class':['comentarii']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['comentarii']})
]
feeds = [
(u'Feeds', u'http://www.bucataras.ro/rss/retete/')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,56 @@
__license__ = 'GPL v3'
__author__ = 'Todd Chapman'
__copyright__ = 'Todd Chapman'
__version__ = 'v0.1'
__date__ = '26 February 2011'
'''
http://www.buffalonews.com/RSS/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1298680852(BasicNewsRecipe):
title = u'Buffalo News'
__author__ = 'ChappyOnIce'
language = 'en'
oldest_article = 2
max_articles_per_feed = 20
encoding = 'utf-8'
remove_javascript = True
keep_only_tags = [
dict(name='div', attrs={'class':['main-content-left']})
]
remove_tags = [
dict(name='div', attrs={'id':['commentCount']}),
dict(name='div', attrs={'class':['story-list-links']})
]
remove_tags_after = dict(name='div', attrs={'class':['body storyContent']})
conversion_options = {
'base_font_size' : 14,
}
feeds = [(u'City of Buffalo', u'http://www.buffalonews.com/city/communities/buffalo/?widget=rssfeed&view=feed&contentId=77944'),
(u'Southern Erie County', u'http://www.buffalonews.com/city/communities/southern-erie/?widget=rssfeed&view=feed&contentId=77944'),
(u'Eastern Erie County', u'http://www.buffalonews.com/city/communities/eastern-erie/?widget=rssfeed&view=feed&contentId=77944'),
(u'Southern Tier', u'http://www.buffalonews.com/city/communities/southern-tier/?widget=rssfeed&view=feed&contentId=77944'),
(u'Niagara County', u'http://www.buffalonews.com/city/communities/niagara-county/?widget=rssfeed&view=feed&contentId=77944'),
(u'Business', u'http://www.buffalonews.com/business/?widget=rssfeed&view=feed&contentId=77944'),
(u'MoneySmart', u'http://www.buffalonews.com/business/moneysmart/?widget=rssfeed&view=feed&contentId=77944'),
(u'Bills & NFL', u'http://www.buffalonews.com/sports/bills-nfl/?widget=rssfeed&view=feed&contentId=77944'),
(u'Sabres & NHL', u'http://www.buffalonews.com/sports/sabres-nhl/?widget=rssfeed&view=feed&contentId=77944'),
(u'Bob DiCesare', u'http://www.buffalonews.com/sports/columns/bob-dicesare/?widget=rssfeed&view=feed&contentId=77944'),
(u'Bucky Gleason', u'http://www.buffalonews.com/sports/columns/bucky-gleason/?widget=rssfeed&view=feed&contentId=77944'),
(u'Mark Gaughan', u'http://www.buffalonews.com/sports/bills-nfl/inside-the-nfl/?widget=rssfeed&view=feed&contentId=77944'),
(u'Mike Harrington', u'http://www.buffalonews.com/sports/columns/mike-harrington/?widget=rssfeed&view=feed&contentId=77944'),
(u'Jerry Sullivan', u'http://www.buffalonews.com/sports/columns/jerry-sullivan/?widget=rssfeed&view=feed&contentId=77944'),
(u'Other Sports Columns', u'http://www.buffalonews.com/sports/columns/other-sports-columns/?widget=rssfeed&view=feed&contentId=77944'),
(u'Life', u'http://www.buffalonews.com/life/?widget=rssfeed&view=feed&contentId=77944'),
(u'Bruce Andriatch', u'http://www.buffalonews.com/city/columns/bruce-andriatch/?widget=rssfeed&view=feed&contentId=77944'),
(u'Donn Esmonde', u'http://www.buffalonews.com/city/columns/donn-esmonde/?widget=rssfeed&view=feed&contentId=77944'),
(u'Rod Watson', u'http://www.buffalonews.com/city/columns/rod-watson/?widget=rssfeed&view=feed&contentId=77944'),
(u'Entertainment', u'http://www.buffalonews.com/entertainment/?widget=rssfeed&view=feed&contentId=77944'),
(u'Off Main Street', u'http://www.buffalonews.com/city/columns/off-main-street/?widget=rssfeed&view=feed&contentId=77944'),
(u'Editorials', u'http://www.buffalonews.com/editorial-page/buffalo-news-editorials/?widget=rssfeed&view=feed&contentId=77944')
]

View File

@ -0,0 +1,27 @@
__license__ = 'GPL v3'
__copyright__ = '2011-2011, Federico Escalada <fedeescalada at gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
class Dotpod(BasicNewsRecipe):
__author__ = 'Federico Escalada'
description = 'Tecnologia y Comunicacion Audiovisual'
encoding = 'utf-8'
language = 'es'
max_articles_per_feed = 100
no_stylesheets = True
oldest_article = 7
publication_type = 'blog'
title = 'Dotpod'
authors = 'Federico Picone'
conversion_options = {
'authors' : authors
,'comments' : description
,'language' : language
}
feeds = [('Dotpod', 'http://www.dotpod.com.ar/feed/')]
remove_tags = [dict(name='div', attrs={'class':'feedflare'})]

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
historia.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class HistoriaRo(BasicNewsRecipe):
title = u'Historia'
__author__ = u'Silviu Cotoar\u0103'
description = ''
publisher = 'Historia'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Istorie'
encoding = 'utf-8'
cover_url = 'http://www.historia.ro/sites/all/themes/historia/images/historia.png'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'class':'c_antet_title'})
, dict(name='a', attrs={'class':'overlaybox'})
, dict(name='div', attrs={'class':'art_content'})
]
remove_tags = [
dict(name='div', attrs={'class':['fl_left']})
, dict(name='div', attrs={'id':['article_toolbar']})
, dict(name='div', attrs={'class':['zoom_cont']})
]
feeds = [
(u'Feeds', u'http://www.historia.ro/rss.xml')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -58,6 +58,21 @@ class FetchNewsAction(InterfaceAction):
self.scheduler.recipe_download_failed(arg)
return self.gui.job_exception(job)
id = self.gui.library_view.model().add_news(pt.name, arg)
# Arg may contain a "keep_issues" variable. If it is non-zero,
# delete all but newest x issues.
try:
keep_issues = int(arg['keep_issues'])
except:
keep_issues = 0
if keep_issues > 0:
ids_with_tag = list(sorted(self.gui.library_view.model().
db.tags_older_than(arg['title'],
None, must_have_tag=_('News')), reverse=True))
ids_to_delete = ids_with_tag[keep_issues:]
if ids_to_delete:
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
self.gui.library_view.model().reset()
sync = self.gui.news_to_be_synced
sync.add(id)

View File

@ -10,11 +10,9 @@ Scheduler for automated recipe downloads
from datetime import timedelta
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QHBoxLayout, \
QLabel
QAction, QIcon, QMutex, QTimer, pyqtSignal
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
from calibre.gui2.search_box import SearchBox2
from calibre.gui2 import config as gconf, error_dialog
from calibre.web.feeds.recipes.model import RecipeModel
from calibre.ptempfile import PersistentTemporaryFile
@ -28,18 +26,12 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.setupUi(self)
self.recipe_model = recipe_model
self.recipe_model.do_refresh()
self.count_label.setText(
_('%s news sources') %
self.recipe_model.showing_count)
self._cont = QWidget(self)
self._cont.l = QHBoxLayout()
self._cont.setLayout(self._cont.l)
self._cont.la = QLabel(_('&Search:'))
self._cont.l.addWidget(self._cont.la, 1)
self.search = SearchBox2(self)
self._cont.l.addWidget(self.search, 100)
self._cont.la.setBuddy(self.search)
self.search.setMinimumContentsLength(25)
self.search.initialize('scheduler_search_history')
self.recipe_box.layout().insertWidget(0, self._cont)
self.search.setMinimumContentsLength(15)
self.search.search.connect(self.recipe_model.search)
self.recipe_model.searched.connect(self.search.search_done,
type=Qt.QueuedConnection)
@ -153,9 +145,12 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.un_schedule_recipe(urn)
add_title_tag = self.add_title_tag.isChecked()
keep_issues = u'0'
if self.keep_issues.isEnabled():
keep_issues = unicode(self.keep_issues.value())
custom_tags = unicode(self.custom_tags.text()).strip()
custom_tags = [x.strip() for x in custom_tags.split(',')]
self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags)
self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags, keep_issues)
return True
def initialize_detail_box(self, urn):
@ -215,9 +210,16 @@ class SchedulerDialog(QDialog, Ui_Dialog):
if d < timedelta(days=366):
self.last_downloaded.setText(_('Last downloaded')+': '+tm)
add_title_tag, custom_tags = customize_info
add_title_tag, custom_tags, keep_issues = customize_info
self.add_title_tag.setChecked(add_title_tag)
self.custom_tags.setText(u', '.join(custom_tags))
try:
keep_issues = int(keep_issues)
except:
keep_issues = 0
self.keep_issues.setValue(keep_issues)
self.keep_issues.setEnabled(self.add_title_tag.isChecked())
class Scheduler(QObject):
@ -299,7 +301,7 @@ class Scheduler(QObject):
un = pw = None
if account_info is not None:
un, pw = account_info
add_title_tag, custom_tags = customize_info
add_title_tag, custom_tags, keep_issues = customize_info
script = self.recipe_model.get_recipe(urn)
pt = PersistentTemporaryFile('_builtin.recipe')
pt.write(script)
@ -312,6 +314,7 @@ class Scheduler(QObject):
'recipe':pt.name,
'title':recipe.get('title',''),
'urn':urn,
'keep_issues':keep_issues
}
self.download_queue.add(urn)
self.start_recipe_fetch.emit(arg)

View File

@ -14,358 +14,403 @@
<string>Schedule news download</string>
</property>
<property name="windowIcon">
<iconset>
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/scheduler.png</normaloff>:/images/scheduler.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" rowspan="3">
<widget class="QGroupBox" name="recipe_box">
<property name="title">
<string>Recipes</string>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1,2">
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&amp;Search:</string>
</property>
<property name="buddy">
<cstring>search</cstring>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="recipes">
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="animated">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="download_all_button">
<property name="toolTip">
<string>Download all scheduled recipes at once</string>
</property>
<property name="text">
<string>Download &amp;all scheduled</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rnumber">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
<widget class="SearchBox2" name="search"/>
</item>
<item row="0" column="2" rowspan="3">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>469</width>
<height>504</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="margin">
<number>0</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>375</width>
<height>502</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="margin">
<item>
<widget class="QTabWidget" name="detail_box">
<property name="currentIndex">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="detail_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Schedule</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Schedule</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="blurb">
<property name="text">
<string>blurb</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="schedule">
<property name="text">
<string>&amp;Schedule for download:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="blurb">
<widget class="QRadioButton" name="daily_button">
<property name="text">
<string>blurb</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="schedule">
<widget class="QComboBox" name="day">
<item>
<property name="text">
<string>day</string>
</property>
</item>
<item>
<property name="text">
<string>Monday</string>
</property>
</item>
<item>
<property name="text">
<string>Tuesday</string>
</property>
</item>
<item>
<property name="text">
<string>Wednesday</string>
</property>
</item>
<item>
<property name="text">
<string>Thursday</string>
</property>
</item>
<item>
<property name="text">
<string>Friday</string>
</property>
</item>
<item>
<property name="text">
<string>Saturday</string>
</property>
</item>
<item>
<property name="text">
<string>Sunday</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Schedule for download:</string>
<string>at</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="daily_button">
<property name="text">
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="day">
<item>
<property name="text">
<string>day</string>
</property>
</item>
<item>
<property name="text">
<string>Monday</string>
</property>
</item>
<item>
<property name="text">
<string>Tuesday</string>
</property>
</item>
<item>
<property name="text">
<string>Wednesday</string>
</property>
</item>
<item>
<property name="text">
<string>Thursday</string>
</property>
</item>
<item>
<property name="text">
<string>Friday</string>
</property>
</item>
<item>
<property name="text">
<string>Saturday</string>
</property>
</item>
<item>
<property name="text">
<string>Sunday</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>at</string>
</property>
</widget>
</item>
<item>
<widget class="QTimeEdit" name="time"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="QTimeEdit" name="time"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="interval_button">
<property name="text">
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="interval">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>365.100000000000023</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="last_downloaded">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="account">
<property name="title">
<string>&amp;Account</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="username"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Username:</string>
</property>
<property name="buddy">
<cstring>username</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>For the scheduling to work, you must leave calibre running.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>&amp;Advanced</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="add_title_tag">
<property name="text">
<string>Add &amp;title as tag</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Extra tags:</string>
</property>
<property name="buddy">
<cstring>custom_tags</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="custom_tags"/>
</item>
<item>
<spacer name="verticalSpacer">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="download_button">
<property name="text">
<string>&amp;Download now</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="interval_button">
<property name="text">
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="interval">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>365.100000000000023</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="last_downloaded">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="account">
<property name="title">
<string>&amp;Account</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="username"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Username:</string>
</property>
<property name="buddy">
<cstring>username</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>For the scheduling to work, you must leave calibre running.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>&amp;Advanced</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QCheckBox" name="add_title_tag">
<property name="text">
<string>Add &amp;title as tag</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Extra tags:</string>
</property>
<property name="buddy">
<cstring>custom_tags</cstring>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="label_6">
<property name="toolTip">
<string>Maximum number of copies (issues) of this recipe to keep. Set to 0 to keep all (disable).</string>
</property>
<property name="text">
<string>&amp;Keep at most:</string>
</property>
<property name="buddy">
<cstring>keep_issues</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="keep_issues">
<property name="toolTip">
<string>&lt;p&gt;When set, this option will cause calibre to keep, at most, the specified number of issues of this periodical. Every time a new issue is downloaded, the oldest one is deleted, if the total is larger than this number.
&lt;p&gt;Note that this feature only works if you have the option to add the title as tag checked, above.
&lt;p&gt;Also, the setting for deleting periodicals older than a number of days, below, takes priority over this setting.</string>
</property>
<property name="specialValueText">
<string>all issues</string>
</property>
<property name="suffix">
<string> issues</string>
</property>
<property name="maximum">
<number>100000</number>
</property>
</widget>
</item>
<item row="4" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="custom_tags"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QPushButton" name="download_button">
<property name="text">
<string>&amp;Download now</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QTreeView" name="recipes">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="animated">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Delete downloaded news older than:</string>
</property>
<property name="buddy">
<cstring>old_news</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="old_news">
<property name="toolTip">
<string>&lt;p&gt;Delete downloaded news older than the specified number of days. Set to zero to disable.
&lt;p&gt;You can also control the maximum number of issues of a specific periodical that are kept by clicking the Advanced tab for that periodical above.</string>
</property>
<property name="specialValueText">
<string>never delete</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<item row="4" column="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -375,24 +420,35 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="old_news">
<item row="4" column="0" colspan="2">
<widget class="QPushButton" name="download_all_button">
<property name="toolTip">
<string>Delete downloaded news older than the specified number of days. Set to zero to disable.</string>
<string>Download all scheduled news sources at once</string>
</property>
<property name="suffix">
<string> days</string>
<property name="text">
<string>Download &amp;all scheduled</string>
</property>
<property name="prefix">
<string>Delete downloaded news older than </string>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="count_label">
<property name="text">
<string/>
</property>
<property name="maximum">
<number>1000</number>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SearchBox2</class>
<extends>QComboBox</extends>
<header>calibre/gui2/search_box.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
@ -436,12 +492,12 @@
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>456</x>
<y>173</y>
<x>458</x>
<y>155</y>
</hint>
<hint type="destinationlabel">
<x>537</x>
<y>176</y>
<x>573</x>
<y>158</y>
</hint>
</hints>
</connection>
@ -452,12 +508,12 @@
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>456</x>
<y>173</y>
<x>458</x>
<y>155</y>
</hint>
<hint type="destinationlabel">
<x>647</x>
<y>176</y>
<x>684</x>
<y>157</y>
</hint>
</hints>
</connection>
@ -468,12 +524,28 @@
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>456</x>
<y>239</y>
<x>458</x>
<y>212</y>
</hint>
<hint type="destinationlabel">
<x>495</x>
<y>218</y>
<x>752</x>
<y>215</y>
</hint>
</hints>
</connection>
<connection>
<sender>add_title_tag</sender>
<signal>toggled(bool)</signal>
<receiver>keep_issues</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>508</x>
<y>42</y>
</hint>
<hint type="destinationlabel">
<x>577</x>
<y>108</y>
</hint>
</hints>
</connection>

View File

@ -178,8 +178,10 @@ class TagCategories(QDialog, Ui_TagCategories):
'multiple periods in a row or spaces before '
'or after periods.')).exec_()
return False
for c in self.categories:
if strcmp(c, cat_name) == 0:
for c in sorted(self.categories.keys(), key=sort_key):
if strcmp(c, cat_name) == 0 or \
(icu_lower(cat_name).startswith(icu_lower(c) + '.') and\
not cat_name.startswith(c + '.')):
error_dialog(self, _('Name already used'),
_('That name is already used, perhaps with different case.')).exec_()
return False

View File

@ -217,11 +217,15 @@ class SearchBox2(QComboBox): # {{{
self.clear()
else:
self.normalize_state()
self.lineEdit().setCompleter(None)
self.setEditText(txt)
self.line_edit.end(False)
if emit_changed:
self.changed.emit()
self._do_search(store_in_history=store_in_history)
c = QCompleter()
self.lineEdit().setCompleter(c)
c.setCompletionMode(c.PopupCompletion)
self.focus_to_library.emit()
finally:
if not store_in_history:

View File

@ -21,6 +21,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE, gprefs
from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.library.database2 import Tag
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, lower, strcmp
from calibre.utils.search_query_parser import saved_searches
@ -69,7 +70,8 @@ class TagDelegate(QItemDelegate): # {{{
# }}}
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2,
'mark_minus': 3, 'mark_minusminus': 4}
class TagsView(QTreeView): # {{{
@ -127,13 +129,17 @@ class TagsView(QTreeView): # {{{
self.set_new_model(self._model.get_filter_categories_by())
def set_database(self, db, tag_match, sort_by):
self.hidden_categories = db.prefs.get('tag_browser_hidden_categories', None)
hidden_cats = db.prefs.get('tag_browser_hidden_categories', None)
self.hidden_categories = []
# migrate from config to db prefs
if self.hidden_categories is None:
self.hidden_categories = config['tag_browser_hidden_categories']
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
else:
self.hidden_categories = set(self.hidden_categories)
if hidden_cats is None:
hidden_cats = config['tag_browser_hidden_categories']
# strip out any non-existence field keys
for cat in hidden_cats:
if cat in db.field_metadata:
self.hidden_categories.append(cat)
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
self.hidden_categories = set(self.hidden_categories)
old = getattr(self, '_model', None)
if old is not None:
@ -370,14 +376,15 @@ class TagsView(QTreeView): # {{{
action='delete_user_category', key=key))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
if not key.startswith('@') or key.find('.') < 0:
self.context_menu.addAction(_('Hide category %s') % category,
partial(self.context_menu_handler, action='hide',
category=category))
#if not key.startswith('@') or key.find('.') < 0:
self.context_menu.addAction(_('Hide category %s') % category,
partial(self.context_menu_handler, action='hide',
category=key))
if self.hidden_categories:
m = self.context_menu.addMenu(_('Show category'))
for col in sorted(self.hidden_categories, key=sort_key):
m.addAction(col,
for col in sorted(self.hidden_categories,
key=lambda x: sort_key(self.db.field_metadata[x]['name'])):
m.addAction(self.db.field_metadata[col]['name'],
partial(self.context_menu_handler, action='show', category=col))
# search by category
@ -540,6 +547,7 @@ class TagTreeItem(object): # {{{
self.id_set = set()
self.is_gst = False
self.boxed = False
self.icon_state_map = list(map(QVariant, icon_map))
if self.parent is not None:
self.parent.append(self)
if data is None:
@ -554,9 +562,11 @@ class TagTreeItem(object): # {{{
self.bold_font = QVariant(self.bold_font)
self.category_key = category_key
self.temporary = temporary
self.tag = Tag(data)
self.tag.is_hierarchical = category_key.startswith('@')
elif self.type == self.TAG:
icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
self.tag = data
if tooltip:
self.tooltip = tooltip + ' '
else:
@ -593,6 +603,8 @@ class TagTreeItem(object): # {{{
if role == Qt.EditRole:
return QVariant(self.py_name)
if role == Qt.DecorationRole:
if self.tag.state:
return self.icon_state_map[self.tag.state]
return self.icon
if role == Qt.FontRole:
return self.bold_font
@ -642,11 +654,21 @@ class TagTreeItem(object): # {{{
'''
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
'''
if self.type == self.TAG:
if set_to is None:
self.tag.state = (self.tag.state + 1)%3
else:
self.tag.state = set_to
if set_to is None:
while True:
self.tag.state = (self.tag.state + 1)%5
if self.tag.state == TAG_SEARCH_STATES['mark_plus'] or \
self.tag.state == TAG_SEARCH_STATES['mark_minus']:
if self.tag.is_editable:
break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']:
if self.tag.is_hierarchical and len(self.children):
break
else:
break
else:
self.tag.state = set_to
def child_tags(self):
res = []
@ -677,7 +699,8 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.drag_drop_finished = drag_drop_finished
self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('minus.png'))]
self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('plusplus.png')),
QIcon(I('minus.png')), QIcon(I('minusminus.png'))]
self.db = db
self.tags_view = parent
self.hidden_categories = hidden_categories
@ -691,26 +714,33 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem()
self.root_item = TagTreeItem(icon_map=self.icon_state_map)
self.category_nodes = []
last_category_node = None
category_node_map = {}
self.category_node_tree = {}
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
for i, key in enumerate(self.row_map):
if self.hidden_categories:
if key in self.hidden_categories:
continue
found = False
for cat in self.hidden_categories:
if cat.startswith('@') and key.startswith(cat + '.'):
found = True
if found:
continue
is_gst = False
if r.startswith('@') and r[1:] in gst:
tt = _(u'The grouped search term name is "{0}"').format(r[1:])
if key.startswith('@') and key[1:] in gst:
tt = _(u'The grouped search term name is "{0}"').format(key[1:])
is_gst = True
elif r == 'news':
elif key == 'news':
tt = ''
else:
tt = _(u'The lookup/search name is "{0}"').format(r)
tt = _(u'The lookup/search name is "{0}"').format(key)
if r.startswith('@'):
path_parts = [p for p in r.split('.')]
if key.startswith('@'):
path_parts = [p for p in key.split('.')]
path = ''
last_category_node = self.root_item
tree_root = self.category_node_tree
@ -719,9 +749,10 @@ class TagsModel(QAbstractItemModel): # {{{
if path not in category_node_map:
node = TagTreeItem(parent=last_category_node,
data=p[1:] if i == 0 else p,
category_icon=self.category_icon_map[r],
tooltip=tt if path == r else path,
category_key=path)
category_icon=self.category_icon_map[key],
tooltip=tt if path == key else path,
category_key=path,
icon_map=self.icon_state_map)
last_category_node = node
category_node_map[path] = node
self.category_nodes.append(node)
@ -736,11 +767,12 @@ class TagsModel(QAbstractItemModel): # {{{
path += '.'
else:
node = TagTreeItem(parent=self.root_item,
data=self.categories[i],
category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r)
data=self.categories[key],
category_icon=self.category_icon_map[key],
tooltip=tt, category_key=key,
icon_map=self.icon_state_map)
node.is_gst = False
category_node_map[r] = node
category_node_map[key] = node
last_category_node = node
self.category_nodes.append(node)
self.refresh(data=data)
@ -1015,7 +1047,7 @@ class TagsModel(QAbstractItemModel): # {{{
def get_node_tree(self, sort):
old_row_map = self.row_map[:]
self.row_map = []
self.categories = []
self.categories = {}
# Get the categories
if self.search_restriction:
@ -1062,7 +1094,7 @@ class TagsModel(QAbstractItemModel): # {{{
for category in tb_categories:
if category in data: # The search category can come and go
self.row_map.append(category)
self.categories.append(tb_categories[category]['name'])
self.categories[category] = tb_categories[category]['name']
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
# A category has been added or removed. We must force a rebuild of
@ -1163,7 +1195,8 @@ class TagsModel(QAbstractItemModel): # {{{
sub_cat = TagTreeItem(parent=category, data = name,
tooltip = None, temporary=True,
category_icon = category_node.icon,
category_key=category_node.category_key)
category_key=category_node.category_key,
icon_map=self.icon_state_map)
self.endInsertRows()
else: # by 'first letter'
cl = cl_list[idx]
@ -1173,7 +1206,8 @@ class TagsModel(QAbstractItemModel): # {{{
data = collapse_letter,
category_icon = category_node.icon,
tooltip = None, temporary=True,
category_key=category_node.category_key)
category_key=category_node.category_key,
icon_map=self.icon_state_map)
node_parent = sub_cat
else:
node_parent = category
@ -1284,16 +1318,19 @@ class TagsModel(QAbstractItemModel): # {{{
return False
user_cats = self.db.prefs.get('user_categories', {})
user_cat_keys_lower = [icu_lower(k) for k in user_cats]
ckey = item.category_key[1:]
ckey_lower = icu_lower(ckey)
dotpos = ckey.rfind('.')
if dotpos < 0:
nkey = val
else:
nkey = ckey[:dotpos+1] + val
for c in user_cats:
if c.startswith(ckey):
nkey_lower = icu_lower(nkey)
for c in sorted(user_cats.keys(), key=sort_key):
if icu_lower(c).startswith(ckey_lower):
if len(c) == len(ckey):
if nkey in user_cats:
if nkey_lower in user_cat_keys_lower:
error_dialog(self.tags_view, _('Rename user category'),
_('The name %s is already used')%nkey, show=True)
return False
@ -1301,7 +1338,7 @@ class TagsModel(QAbstractItemModel): # {{{
del user_cats[ckey]
elif c[len(ckey)] == '.':
rest = c[len(ckey):]
if (nkey + rest) in user_cats:
if icu_lower(nkey + rest) in user_cat_keys_lower:
error_dialog(self.tags_view, _('Rename user category'),
_('The name %s is already used')%(nkey+rest), show=True)
return False
@ -1477,16 +1514,15 @@ class TagsModel(QAbstractItemModel): # {{{
def reset_all_states(self, except_=None):
update_list = []
def process_tag(tag_item):
if tag_item.type != TagTreeItem.CATEGORY:
tag = tag_item.tag
if tag is except_:
tag_index = self.createIndex(tag_item.row(), 0, tag_item)
self.dataChanged.emit(tag_index, tag_index)
elif tag.state != 0 or tag in update_list:
tag_index = self.createIndex(tag_item.row(), 0, tag_item)
tag.state = 0
update_list.append(tag)
self.dataChanged.emit(tag_index, tag_index)
tag = tag_item.tag
if tag is except_:
tag_index = self.createIndex(tag_item.row(), 0, tag_item)
self.dataChanged.emit(tag_index, tag_index)
elif tag.state != 0 or tag in update_list:
tag_index = self.createIndex(tag_item.row(), 0, tag_item)
tag.state = 0
update_list.append(tag)
self.dataChanged.emit(tag_index, tag_index)
for t in tag_item.children:
process_tag(t)
@ -1503,13 +1539,11 @@ class TagsModel(QAbstractItemModel): # {{{
'''
if not index.isValid(): return False
item = index.internalPointer()
if item.type == TagTreeItem.TAG:
item.toggle(set_to=set_to)
if exclusive:
self.reset_all_states(except_=item.tag)
self.dataChanged.emit(index, index)
return True
return False
item.toggle(set_to=set_to)
if exclusive:
self.reset_all_states(except_=item.tag)
self.dataChanged.emit(index, index)
return True
def tokens(self):
ans = []
@ -1523,19 +1557,31 @@ class TagsModel(QAbstractItemModel): # {{{
# into the search string only once. The nodes_seen set helps us do that
nodes_seen = set()
node_searches = {TAG_SEARCH_STATES['mark_plus'] : 'true',
TAG_SEARCH_STATES['mark_plusplus'] : '.true',
TAG_SEARCH_STATES['mark_minus'] : 'false',
TAG_SEARCH_STATES['mark_minusminus'] : '.false'}
for node in self.category_nodes:
if node.tag.state:
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
key = node.category_key
for tag_item in node.child_tags():
tag = tag_item.tag
if tag.state != TAG_SEARCH_STATES['clear']:
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
else ''
if tag.state == TAG_SEARCH_STATES['mark_minus'] or \
tag.state == TAG_SEARCH_STATES['mark_minusminus']:
prefix = ' not '
else:
prefix = ''
category = tag.category if key != 'news' else 'tag'
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else:
name = original_name(tag)
use_prefix = tag.is_hierarchical
use_prefix = tag.state in [TAG_SEARCH_STATES['mark_plusplus'],
TAG_SEARCH_STATES['mark_minusminus']]
if category == 'tags':
if name in tags_seen:
continue

View File

@ -419,28 +419,23 @@ class ResultCache(SearchQueryParser): # {{{
def get_user_category_matches(self, location, query, candidates):
res = set([])
if self.db_prefs is None:
if self.db_prefs is None or len(query) < 2:
return res
user_cats = self.db_prefs.get('user_categories', [])
c = set(candidates)
l = location.rfind('.')
if l > 0:
alt_loc = location[0:l]
alt_item = location[l+1:]
if query.startswith('.'):
check_subcats = True
query = query[1:]
else:
alt_loc = None
check_subcats = False
for key in user_cats:
if key == location or key.startswith(location + '.'):
if key == location or (check_subcats and key.startswith(location + '.')):
for (item, category, ign) in user_cats[key]:
s = self.get_matches(category, '=' + item, candidates=c)
c -= s
res |= s
elif key == alt_loc:
for (item, category, ign) in user_cats[key]:
if item == alt_item:
s = self.get_matches(category, '=' + item, candidates=c)
c -= s
res |= s
if query == 'false':
return candidates - res
return res

View File

@ -1195,7 +1195,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
i += 1
else:
new_cats['.'.join(comps)] = user_cats[k]
self.prefs.set('user_categories', new_cats)
try:
self.prefs.set('user_categories', new_cats)
except:
pass
return new_cats
def get_categories(self, sort='name', ids=None, icon_map=None):
@ -1500,18 +1503,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
############# End get_categories
def tags_older_than(self, tag, delta):
def tags_older_than(self, tag, delta, must_have_tag=None):
'''
Return the ids of all books having the tag ``tag`` that are older than
than the specified time. tag comparison is case insensitive.
:param delta: A timedelta object or None. If None, then all ids with
the tag are returned.
:param must_have_tag: If not None the list of matches will be
restricted to books that have this tag
'''
tag = tag.lower().strip()
mht = must_have_tag.lower().strip() if must_have_tag else None
now = nowf()
tindex = self.FIELD_MAP['timestamp']
gindex = self.FIELD_MAP['tags']
iindex = self.FIELD_MAP['id']
for r in self.data._data:
if r is not None:
if (now - r[tindex]) > delta:
if delta is None or (now - r[tindex]) > delta:
tags = r[gindex]
if tags and tag in [x.strip() for x in
tags.lower().split(',')]:
yield r[self.FIELD_MAP['id']]
if tags:
tags = [x.strip() for x in tags.lower().split(',')]
if tag in tags and (mht is None or mht in tags):
yield r[iindex]
def get_next_series_num_for(self, series):
series_id = self.conn.get('SELECT id from series WHERE name=?',

View File

@ -436,25 +436,26 @@ Tag Browser
.. image:: images/tag_browser.png
:class: float-left-img
The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any Item in the Tag Browser, for example, the Author name, Isaac Asimov, then the list of books to the right is restricted to books by that author. Clicking once again on Isaac Asimov will restrict the list of books to books not by Isaac Asimov. A third click will remove the restriction. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could Hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. It is a good way to learn how to construct basic search expressions.
The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any item in the Tag Browser, for example the author name Isaac Asimov, then the list of books to the right is restricted to showing books by that author. You can click on category names as well. For example, clicking on "Series" will show you all books in any series.
There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose to hide it or rename it or open a "Manage x" dialog that allows you to manage items of that kind. For example the "Manage Authors" dialog allows you to rename authors and control how their names are sorted.
The first click on an item will restrict the list of books to those that contain/match the item. Continuing the above example, clicking on Isaac Asimov will show books by that author. Clicking again on the item will change what is shown, depending on whether the item has children (see sub-categories and hierarchical items below). Continuing the Isaac Asimov example, clicking again on Isaac Asimov will restrict the list of books to those not by Isaac Asimov. A third click will remove the restriction, showing all books. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. Looking at what the Tag Browser generates is a good way to learn how to construct basic search expressions.
Items in the Tag browser have their icons partially colored. The amount of color depends on the average rating of the books in that category. So for example if the books by Isaac Asimov have an average of four stars, the icon for Isaac Asimov in the Tag Browser will be 4/5th colored. You can hover your mouse over the icon to see the average rating.
For convenience, you can drag and drop books from the book list to items in the Tag Browser and that item will be automatically applied to the dropped books. For example, dragging a book to Isaac Asimov will set the author of that book to Isaac Asimov or dragging it to the tag History will add the tag History to its tags.
The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the User Categories Editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the sub-category name; or by using the User Categories Editor by entering names like the Favorites example above.
The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the user categories editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories act like built-in categories; you can click on items to search for them. You can search for all items in a category by right-clicking on the category name and choosing "Search for books in ...".
You can search user categories in the same way as built-in categories, by clicking on them. There are four different searches cycled through by clicking: "everything matching an item in the category" indicated by a single green plus sign, "everything matching an item in the category or its sub-categories" indicated by two green plus signs, "everything not matching an item in the category" shown by a single red minus sign, and "everything not matching an item in the category or its sub-categories" shown by two red minus signs.
User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created using Manage User Categories by entering names like the Favorites example. They can also be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the category name.
It is also possible to create hierarchies inside some of the text categories such as tags, series, and custom columns. These hierarchies show with the small triangle, permitting the sub-items to be hidden. To use hierarchies of items in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items.
It is also possible to create hierarchies inside some of the built-in categories (the text categories). These hierarchies show with the small triangle permitting the sub-items to be hidden. To use hierarchies in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items.
Hierarchical items (items with children) use the same four 'click-on' searches as user categories. Items that do not have children use two of the searches: "everything matching" and "everything not matching".
You can drag and drop items in the Tag browser onto user categories to add them to that category.
You can drag and drop items in the Tag browser onto user categories to add them to that category. If the source is a user category, holding the shift key while dragging will move the item to the new category. You can also drag and drop books from the book list onto items in the Tag Browser; dropping a book on an item causes that item to be automatically applied to the dropped books. For example, dragging a book onto Isaac Asimov will set the author of that book to Isaac Asimov. Dropping it onto the tag History will add the tag History to the book's tags.
There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose one of several operations. Some examples are to hide the it, rename it, or open a "Manage x" dialog that allows you to manage items of that kind. For example, the "Manage Authors" dialog allows you to rename authors and control how their names are sorted.
You can control how items are sorted in the Tag browser via the box at the bottom of the Tag Browser. You can choose to sort by name, average rating or popularity (popularity is the number of books with an item in your library; for example; the popularity of Isaac Asimov is the number of book sin your library by Isaac Asimov).
Jobs
-----
.. image:: images/jobs.png

View File

@ -201,12 +201,14 @@ class SchedulerConfig(object):
self.root.append(sr)
self.write_scheduler_file()
def customize_recipe(self, urn, add_title_tag, custom_tags):
# 'keep_issues' argument for recipe-specific number of copies to keep
def customize_recipe(self, urn, add_title_tag, custom_tags, keep_issues):
with self.lock:
for x in list(self.iter_customization()):
if x.get('id') == urn:
self.root.remove(x)
cs = E.recipe_customization({
'keep_issues' : keep_issues,
'id' : urn,
'add_title_tag' : 'yes' if add_title_tag else 'no',
'custom_tags' : ','.join(custom_tags),
@ -317,16 +319,18 @@ class SchedulerConfig(object):
return x.get('username', ''), x.get('password', '')
def get_customize_info(self, urn):
keep_issues = 0
add_title_tag = True
custom_tags = []
with self.lock:
for x in self.iter_customization():
if x.get('id', False) == urn:
keep_issues = x.get('keep_issues', '0')
add_title_tag = x.get('add_title_tag', 'yes') == 'yes'
custom_tags = [i.strip() for i in x.get('custom_tags',
'').split(',')]
break
return add_title_tag, custom_tags
return add_title_tag, custom_tags, keep_issues
def get_schedule_info(self, urn):
with self.lock:

View File

@ -196,6 +196,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
lang_map = {}
self.all_urns = set([])
self.showing_count = 0
self.builtin_count = 0
for x in self.custom_recipe_collection:
urn = x.get('id')
self.all_urns.add(urn)
@ -211,6 +212,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
lang_map[lang] = factory(NewsCategory, new_root, lang)
factory(NewsItem, lang_map[lang], urn, x.get('title'))
self.showing_count += 1
self.builtin_count += 1
for x in self.scheduler_config.iter_recipes():
urn = x.get('id')
if urn not in self.all_urns:
@ -354,9 +356,9 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
self.scheduler_config.schedule_recipe(self.recipe_from_urn(urn),
sched_type, schedule)
def customize_recipe(self, urn, add_title_tag, custom_tags):
def customize_recipe(self, urn, add_title_tag, custom_tags, keep_issues):
self.scheduler_config.customize_recipe(urn, add_title_tag,
custom_tags)
custom_tags, keep_issues)
def get_to_be_downloaded_recipes(self):
ans = self.scheduler_config.get_to_be_downloaded_recipes()