Close adomasven/zotero#11. Add support for feed imports from OPML files

This commit is contained in:
Adomas Venčkauskas 2016-04-20 15:09:31 +01:00
parent 3dabd63a0a
commit 3b758e562b
9 changed files with 119 additions and 2 deletions

View file

@ -236,6 +236,8 @@ Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this._feedName) throw new Error("Feed name not set");
if (!this._feedUrl) throw new Error("Feed URL not set");
if (!this.refreshInterval) this.refreshInterval = Zotero.Prefs.get('feeds.defaultTTL') * 60;
if (!this.cleanupAfter) this.cleanupAfter = Zotero.Prefs.get('feeds.defaultCleanupAfter');
if (env.isNew) {
// Make sure URL is unique

View file

@ -73,6 +73,44 @@ Zotero.Feeds = new function() {
return this.scheduleNextFeedCheck();
}
this.importFromOPML = Zotero.Promise.coroutine(function* (opmlString) {
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser);
var doc = parser.parseFromString(opmlString, "application/xml");
// Per some random spec (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser),
// DOMParser returns a special type of xml document on error, so we do some magic checking here.
if (doc.documentElement.tagName == 'parseerror') {
return false;
}
var body = doc.getElementsByTagName('body')[0];
var feedElems = doc.querySelectorAll('[type=rss][url], [xmlUrl]');
var newFeeds = [];
var registeredUrls = new Set();
for (let feedElem of feedElems) {
let url = feedElem.getAttribute('xmlUrl');
if (!url) url = feedElem.getAttribute('url');
let name = feedElem.getAttribute('title');
if (!name) name = feedElem.getAttribute('text');
if (Zotero.Feeds.existsByURL(url) || registeredUrls.has(url)) {
Zotero.debug("Feed Import from OPML: Feed " + name + " : " + url + " already exists. Skipping");
continue;
}
// Prevent duplicates from the same OPML file
registeredUrls.add(url);
let feed = new Zotero.Feed({url, name});
newFeeds.push(feed);
}
// This could potentially be a massive list, so we save in a transaction.
yield Zotero.DB.executeTransaction(function* () {
for (let feed of newFeeds) {
yield feed.save();
}
});
// Finally, update
yield Zotero.Feeds.updateFeeds();
return true;
});
this.restoreFromJSON = Zotero.Promise.coroutine(function* (json, merge=false) {
Zotero.debug("Restoring feeds from remote JSON");
Zotero.debug(json);

View file

@ -870,6 +870,27 @@ var ZoteroPane = new function()
return collection.saveTx();
});
this.importFeedsFromOPML = Zotero.Promise.coroutine(function* (event) {
var nsIFilePicker = Components.interfaces.nsIFilePicker;
while (true) {
var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(window, Zotero.getString('fileInterface.importOPML'), nsIFilePicker.modeOpen);
fp.appendFilter(Zotero.getString('fileInterface.OPMLFeedFilter'), '*.opml; *.xml');
fp.appendFilters(nsIFilePicker.filterAll);
if (fp.show() == nsIFilePicker.returnOK) {
var contents = yield Zotero.File.getContentsAsync(fp.file.path);
var success = yield Zotero.Feeds.importFromOPML(contents);
if (success) {
return true;
}
// Try again
Zotero.alert(window, Zotero.getString('general.error'), Zotero.getString('fileInterface.unsupportedFormat'));
} else {
return false;
}
}
});
this.newFeedFromPage = Zotero.Promise.coroutine(function* (event) {
let data = {unsaved: true};
if (event) {

View file

@ -117,6 +117,8 @@
<menupopup oncommand="ZoteroPane_Local.newFeedFromPage(event)"
onpopupshowing="FeedHandler.buildFeedList(event.target)"/>
</menu>
<menuitem id="zotero-tb-feed-add-fromOPML" label="&zotero.toolbar.feeds.new.fromOPML;"
oncommand="ZoteroPane_Local.importFeedsFromOPML()"/>
</menupopup>
</menu>
</menupopup>

View file

@ -127,7 +127,8 @@
<!ENTITY zotero.toolbar.feeds.new "New Feed…">
<!ENTITY zotero.toolbar.feeds.new.fromURL "From URL…">
<!ENTITY zotero.toolbar.feeds.new.fromPage "From Page…">
<!ENTITY zotero.toolbar.feeds.new.fromPage "From Page…">
<!ENTITY zotero.toolbar.feeds.new.fromOPML "From OPML…">
<!ENTITY zotero.toolbar.feeds.refresh "Refresh Feed">
<!ENTITY zotero.toolbar.feeds.edit "Edit Feed…">

View file

@ -637,6 +637,8 @@ fileInterface.importClipboardNoDataError = No importable data could be read from
fileInterface.noReferencesError = The items you have selected contain no references. Please select one or more references and try again.
fileInterface.bibliographyGenerationError = An error occurred generating your bibliography. Please try again.
fileInterface.exportError = An error occurred while trying to export the selected file.
fileInterface.importOPML = Import Feeds from OPML
fileInterface.OPMLFeedFilter = OPML Feed List
quickSearch.mode.titleCreatorYear = Title, Creator, Year
quickSearch.mode.fieldsAndTags = All Fields & Tags

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>An OPML file with a list of rss/atom feeds</title>
<docs>The OPML format is fairly poorly spec'ed out here http://dev.opml.org/spec2.html</docs>
</head>
<body>
<outline text="Standard format">
<outline type="rss" text="A title 1" title="A title 1" xmlUrl="http://example.com/feed1.rss"/>
<outline type="rss" text="A title 2" title="A title 2" xmlUrl="http://example.com/feed2.rss"/>
</outline>
<outline text="Non-standard format">
<outline type="rss" text="A title 3" title="A title 3" url="http://example.com/feed3.rss"/>
<outline type="rss" text="A title 4" title="A title 4" url="http://example.com/feed4.rss"/>
</outline>
</body>
</opml>

View file

@ -4,6 +4,39 @@ describe("Zotero.Feeds", function () {
yield clearFeeds();
});
describe('#importFromOPML', function() {
var opmlUrl = getTestDataUrl("feeds.opml");
var opmlString;
before(function* (){
opmlString = yield Zotero.File.getContentsFromURLAsync(opmlUrl)
});
beforeEach(function* () {
yield clearFeeds();
});
it('imports feeds correctly', function* (){
let shouldExist = {
"http://example.com/feed1.rss": "A title 1",
"http://example.com/feed2.rss": "A title 2",
"http://example.com/feed3.rss": "A title 3",
"http://example.com/feed4.rss": "A title 4"
}; yield Zotero.Feeds.importFromOPML(opmlString);
let feeds = Zotero.Feeds.getAll();
for (let feed of feeds) {
assert.equal(shouldExist[feed.url], feed.name, "Feed exists and title matches");
delete shouldExist[feed.url];
}
assert.equal(Object.keys(shouldExist).length, 0, "All feeds from opml have been created");
});
it("doesn't fail if some feeds already exist", function* (){
yield createFeed({url: "http://example.com/feed1.rss"});
yield Zotero.Feeds.importFromOPML(opmlString)
});
});
describe("#restoreFromJSON", function() {
var json = {};
var expiredFeedURL, existingFeedURL;

@ -1 +1 @@
Subproject commit c834d847805f030afe3c38c4ad92f135faf01a4d
Subproject commit f0aa00219aeb75f04654a59339aa94e1accbc956