mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Added OPML support to the user_profile dialog
This commit is contained in:
parent
26fd1f8803
commit
05a566c275
BIN
resources/images/opml.png
Normal file
BIN
resources/images/opml.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -8,11 +8,13 @@ from PyQt4.Qt import (QUrl, QAbstractListModel, Qt, QVariant, QFont)
|
||||
from calibre.web.feeds.recipes import compile_recipe, custom_recipes
|
||||
from calibre.web.feeds.news import AutomaticNewsRecipe
|
||||
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
||||
from calibre.gui2.dialogs.message_box import MessageBox
|
||||
from calibre.gui2 import error_dialog, question_dialog, open_url, \
|
||||
choose_files, ResizableDialog, NONE, open_local_file
|
||||
from calibre.gui2.widgets import PythonHighlighter
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.opml import OPML
|
||||
|
||||
class CustomRecipeModel(QAbstractListModel):
|
||||
|
||||
@ -90,6 +92,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
||||
self.remove_profile_button.clicked[(bool)].connect(self.remove_selected_items)
|
||||
self.add_feed_button.clicked[(bool)].connect(self.add_feed)
|
||||
self.load_button.clicked[()].connect(self.load)
|
||||
self.opml_button.clicked[()].connect(self.opml_import)
|
||||
self.builtin_recipe_button.clicked[()].connect(self.add_builtin_recipe)
|
||||
self.share_button.clicked[()].connect(self.share)
|
||||
self.show_recipe_files_button.clicked.connect(self.show_recipe_files)
|
||||
@ -201,15 +204,20 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
||||
self.feed_title.setText('')
|
||||
self.feed_url.setText('')
|
||||
|
||||
def options_to_profile(self):
|
||||
def options_to_profile(self, **kw):
|
||||
classname = 'BasicUserRecipe'+str(int(time.time()))
|
||||
title = unicode(self.profile_title.text()).strip()
|
||||
if 'nr' in kw:
|
||||
classname = classname + str(kw['nr'])
|
||||
title = kw['title'] if 'title' in kw else self.profile_title.text()
|
||||
title = unicode(title).strip()
|
||||
if not title:
|
||||
title = classname
|
||||
self.profile_title.setText(title)
|
||||
oldest_article = self.oldest_article.value()
|
||||
max_articles = self.max_articles.value()
|
||||
feeds = [i.user_data for i in self.added_feeds.items()]
|
||||
oldest_article = kw['oldest_article'] if 'oldest_article' in kw else self.oldest_article.value()
|
||||
max_articles = kw['max_articles'] if 'max_articles' in kw else self.max_articles.value()
|
||||
feeds = kw['feeds'] \
|
||||
if 'feeds' in kw \
|
||||
else [i.user_data for i in self.added_feeds.items()]
|
||||
|
||||
src = '''\
|
||||
class %(classname)s(%(base_class)s):
|
||||
@ -346,6 +354,50 @@ class %(classname)s(%(base_class)s):
|
||||
self.model.add(title, profile)
|
||||
self.clear()
|
||||
|
||||
def opml_import(self):
|
||||
opml_files = choose_files(self, 'OPML chooser dialog',
|
||||
_('Select OPML file'), filters=[(_('OPML'), ['opml'])] )
|
||||
|
||||
if not opml_files:
|
||||
return
|
||||
|
||||
opml = OPML(self.oldest_article.value(), self.max_articles.value());
|
||||
for opml_file in opml_files:
|
||||
opml.load(opml_file)
|
||||
outlines = opml.parse()
|
||||
nr = 0
|
||||
for outline in outlines:
|
||||
src, title = self.options_to_profile(**{
|
||||
'nr':nr,
|
||||
'title':unicode(outline.get('title')),
|
||||
'feeds':outline.get('xmlUrl'),
|
||||
'oldest_article':self.oldest_article.value(),
|
||||
'max_articles':self.max_articles.value(),
|
||||
})
|
||||
|
||||
try:
|
||||
compile_recipe(src)
|
||||
except Exception as err:
|
||||
error_dialog(self, _('Invalid input'),
|
||||
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
|
||||
return
|
||||
profile = src
|
||||
if self._model.has_title(title):
|
||||
if question_dialog(self, _('Replace recipe?'),
|
||||
_('A custom recipe named %s already exists. Do you want to '
|
||||
'replace it?')%title):
|
||||
self._model.replace_by_title(title, profile)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
self.model.add(title, profile)
|
||||
nr+=1
|
||||
self.clear()
|
||||
|
||||
msg_box = MessageBox(MessageBox.INFO, "Finished", "OPML to Recipe conversion complete", parent=self,
|
||||
show_copy_button=False)
|
||||
msg_box.exec_()
|
||||
|
||||
def populate_options(self, profile):
|
||||
self.oldest_article.setValue(profile.oldest_article)
|
||||
self.max_articles.setValue(profile.max_articles_per_feed)
|
||||
|
@ -135,6 +135,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="opml_button">
|
||||
<property name="text">
|
||||
<string>&Import from OPML files</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/opml.png</normaloff>:/images/opml.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
87
src/calibre/utils/opml.py
Normal file
87
src/calibre/utils/opml.py
Normal file
@ -0,0 +1,87 @@
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2014, Kenny Billiau <kennybilliau@gmail.co'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class OPML(object):
|
||||
|
||||
def __init__(self, oldest_article = 7, max_articles = 100):
|
||||
self.doc = None # xml document
|
||||
self.outlines = None # parsed outline objects
|
||||
self.oldest_article = oldest_article
|
||||
self.max_articles = max_articles
|
||||
|
||||
def load(self, filename):
|
||||
tree = ET.parse(filename)
|
||||
self.doc = tree.getroot()
|
||||
|
||||
def parse(self):
|
||||
self.outlines = self.doc.findall(u"body/outline")
|
||||
|
||||
for outline in self.outlines: # check for groups
|
||||
#if ('type' not in outline.attrib):
|
||||
feeds = [] # title, url
|
||||
for feed in outline.iter('outline'):
|
||||
if 'type' in feed.attrib:
|
||||
feeds.append( (feed.get('title'), feed.get('xmlUrl')) )
|
||||
outline.set('xmlUrl', feeds)
|
||||
|
||||
return self.outlines
|
||||
|
||||
def import_recipes(self, outlines):
|
||||
nr = 0
|
||||
#recipe_model = CustomRecipeModel(RecipeModel())
|
||||
for outline in outlines:
|
||||
src, title = self.options_to_profile(dict(
|
||||
nr=nr,
|
||||
title=unicode(outline.get('title')),
|
||||
feeds=outline.get('xmlUrl'),
|
||||
oldest_article=self.oldest_article,
|
||||
max_articles=self.max_articles,
|
||||
base_class='AutomaticNewsRecipe'
|
||||
))
|
||||
try:
|
||||
compile_recipe(src)
|
||||
add_custom_recipe(title, src)
|
||||
except Exception as err:
|
||||
# error dialog should be placed somewhere where it can have a parent
|
||||
# Left it here as this way only failing feeds will silently fail
|
||||
error_dialog(None, _('Invalid input'),
|
||||
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
|
||||
nr+=1
|
||||
|
||||
#recipe_model.add(title, src)
|
||||
|
||||
|
||||
def options_to_profile(self, recipe):
|
||||
classname = 'BasicUserRecipe'+str(recipe.get('nr'))+str(int(time.time()))
|
||||
title = recipe.get('title').strip()
|
||||
if not title:
|
||||
title = classname
|
||||
oldest_article = self.oldest_article
|
||||
max_articles = self.max_articles
|
||||
feeds = recipe.get('feeds')
|
||||
|
||||
src = '''\
|
||||
class %(classname)s(%(base_class)s):
|
||||
title = %(title)s
|
||||
oldest_article = %(oldest_article)d
|
||||
max_articles_per_feed = %(max_articles)d
|
||||
auto_cleanup = True
|
||||
|
||||
feeds = %(feeds)s
|
||||
'''%dict(classname=classname, title=repr(title),
|
||||
feeds=repr(feeds), oldest_article=oldest_article,
|
||||
max_articles=max_articles,
|
||||
base_class='AutomaticNewsRecipe')
|
||||
return src, title
|
||||
|
||||
if __name__ == '__main__':
|
||||
opml = OPML();
|
||||
opml.load('/media/sf_Kenny/Downloads/feedly.opml')
|
||||
outlines = opml.parse()
|
||||
print(len(opml.outlines))
|
||||
opml.import_recipes(outlines)
|
||||
|
Loading…
x
Reference in New Issue
Block a user