Close adomasven/zotero#11. Add support for feed imports from OPML files
This commit is contained in:
parent
3dabd63a0a
commit
3b758e562b
|
@ -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._feedName) throw new Error("Feed name not set");
|
||||||
if (!this._feedUrl) throw new Error("Feed URL 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) {
|
if (env.isNew) {
|
||||||
// Make sure URL is unique
|
// Make sure URL is unique
|
||||||
|
|
|
@ -73,6 +73,44 @@ Zotero.Feeds = new function() {
|
||||||
return this.scheduleNextFeedCheck();
|
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) {
|
this.restoreFromJSON = Zotero.Promise.coroutine(function* (json, merge=false) {
|
||||||
Zotero.debug("Restoring feeds from remote JSON");
|
Zotero.debug("Restoring feeds from remote JSON");
|
||||||
Zotero.debug(json);
|
Zotero.debug(json);
|
||||||
|
|
|
@ -870,6 +870,27 @@ var ZoteroPane = new function()
|
||||||
return collection.saveTx();
|
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) {
|
this.newFeedFromPage = Zotero.Promise.coroutine(function* (event) {
|
||||||
let data = {unsaved: true};
|
let data = {unsaved: true};
|
||||||
if (event) {
|
if (event) {
|
||||||
|
|
|
@ -117,6 +117,8 @@
|
||||||
<menupopup oncommand="ZoteroPane_Local.newFeedFromPage(event)"
|
<menupopup oncommand="ZoteroPane_Local.newFeedFromPage(event)"
|
||||||
onpopupshowing="FeedHandler.buildFeedList(event.target)"/>
|
onpopupshowing="FeedHandler.buildFeedList(event.target)"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
<menuitem id="zotero-tb-feed-add-fromOPML" label="&zotero.toolbar.feeds.new.fromOPML;"
|
||||||
|
oncommand="ZoteroPane_Local.importFeedsFromOPML()"/>
|
||||||
</menupopup>
|
</menupopup>
|
||||||
</menu>
|
</menu>
|
||||||
</menupopup>
|
</menupopup>
|
||||||
|
|
|
@ -127,7 +127,8 @@
|
||||||
|
|
||||||
<!ENTITY zotero.toolbar.feeds.new "New Feed…">
|
<!ENTITY zotero.toolbar.feeds.new "New Feed…">
|
||||||
<!ENTITY zotero.toolbar.feeds.new.fromURL "From URL…">
|
<!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.refresh "Refresh Feed">
|
||||||
<!ENTITY zotero.toolbar.feeds.edit "Edit Feed…">
|
<!ENTITY zotero.toolbar.feeds.edit "Edit Feed…">
|
||||||
|
|
||||||
|
|
|
@ -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.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.bibliographyGenerationError = An error occurred generating your bibliography. Please try again.
|
||||||
fileInterface.exportError = An error occurred while trying to export the selected file.
|
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.titleCreatorYear = Title, Creator, Year
|
||||||
quickSearch.mode.fieldsAndTags = All Fields & Tags
|
quickSearch.mode.fieldsAndTags = All Fields & Tags
|
||||||
|
|
18
test/tests/data/feeds.opml
Normal file
18
test/tests/data/feeds.opml
Normal 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>
|
|
@ -4,6 +4,39 @@ describe("Zotero.Feeds", function () {
|
||||||
yield clearFeeds();
|
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() {
|
describe("#restoreFromJSON", function() {
|
||||||
var json = {};
|
var json = {};
|
||||||
var expiredFeedURL, existingFeedURL;
|
var expiredFeedURL, existingFeedURL;
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c834d847805f030afe3c38c4ad92f135faf01a4d
|
Subproject commit f0aa00219aeb75f04654a59339aa94e1accbc956
|
Loading…
Reference in New Issue
Block a user