├── LICENSE ├── README.md ├── evernote-bookmarks └── tests ├── resources ├── missing_source_url_a_href.html ├── missing_source_url_a_href.xml ├── missing_source_url_a_no_href.html ├── missing_source_url_a_no_href.xml ├── well_formed_note.html └── well_formed_note.xml └── test-evernote-bookmarks.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jason Barrie Morley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | evernote-bookmarks 2 | ================== 3 | 4 | Convert Evernote XML Format (.enx) to the Netscape Bookmark File Format (bookmarks.html). 5 | 6 | Usage 7 | ----- 8 | 9 | ``` 10 | usage: evernote-bookmarks [-h] input output 11 | 12 | Convert Evernote XML Format (.enx) to the Netscape Bookmark File Format 13 | (bookmarks.html). 14 | 15 | positional arguments: 16 | input Evernote .enx file to convert 17 | output Output bookmarks.html file 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | ``` 22 | 23 | Changelog 24 | --------- 25 | 26 | ### Version 1.0.1 27 | 28 | - FIX: Crashes on notes with no source URL. 29 | 30 | ### Version 1.0.0 31 | 32 | - Initial release. 33 | 34 | Thanks 35 | ------ 36 | 37 | Many thanks to: 38 | 39 | - [Christophe Pelé](https://github.com/geekarist/) for bug fixes 40 | 41 | License 42 | ------- 43 | 44 | evernote-bookmarks is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 45 | -------------------------------------------------------------------------------- /evernote-bookmarks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014 Jason Barrie Morley 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import argparse 24 | import codecs 25 | import datetime 26 | import logging 27 | import time 28 | import xml.etree.ElementTree as ElementTree 29 | 30 | 31 | def datetime_to_epoch(dt): 32 | epoch = datetime.datetime(1970,1,1) 33 | et = (dt - epoch).total_seconds() 34 | return int(et) 35 | 36 | 37 | def timestamp_to_datetime(ts): 38 | t = time.strptime(ts, "%Y%m%dT%H%M%SZ") 39 | dt = datetime.datetime.fromtimestamp(time.mktime(t)) 40 | return dt 41 | 42 | 43 | def timestamp_to_epoch(ts): 44 | dt = timestamp_to_datetime(ts) 45 | et = datetime_to_epoch(dt) 46 | return et 47 | 48 | 49 | def find_node_recursive(root, search): 50 | results = root.findall(search) 51 | for child in root.getchildren(): 52 | results.extend(find_node_recursive(child, search)) 53 | return results 54 | 55 | 56 | def convert(notes_file, bookmarks_file): 57 | errors = 0 58 | 59 | # Read the evernote export. 60 | bookmarks = [] 61 | tree = ElementTree.parse(notes_file) 62 | for note in tree.getroot().findall('note'): 63 | title = note.find('title').text 64 | created = note.find('created').text 65 | tags = map(lambda x: x.text, note.findall('tag')) 66 | 67 | url = None 68 | try: 69 | url = note.find('note-attributes').find('source-url').text 70 | except AttributeError: 71 | content = note.find('content').text 72 | en_note = ElementTree.fromstring(content.encode('utf-8')) 73 | 74 | links = find_node_recursive(en_note, 'a') 75 | if links: 76 | a = links[0] 77 | if 'href' in a.attrib: 78 | url = a.attrib['href'] 79 | else: 80 | url = a.text 81 | logging.warn("Unable to find 'source-url' in '%s'. Using first URL found in note content.", title) 82 | 83 | if url is None: 84 | logging.warn("Unable to find URL for '%s'...", title) 85 | errors = errors + 1 86 | continue 87 | 88 | bookmarks.append({'title': title, 'created': created, 'url': url, 'tags': tags}) 89 | 90 | # Write the bookmarks.html file. 91 | count = 0 92 | with codecs.open(bookmarks_file, 'w', 'utf-8') as f: 93 | f.write(""" 94 | 97 | Bookmarks 98 |

Bookmarks

99 |
100 | """) 101 | 102 | for bookmark in bookmarks: 103 | logging.debug("Converted '%s'.", bookmark['title']) 104 | result = u"
%s\n" % (bookmark['url'], timestamp_to_epoch(bookmark['created']), ", ".join(bookmark['tags']), bookmark['title']) 105 | f.write(result) 106 | count += 1 107 | 108 | f.write("
\n") 109 | 110 | logging.info("Processed %d bookmarks. Failed to import %d bookmarks.", count, errors) 111 | 112 | 113 | def main(): 114 | parser = argparse.ArgumentParser(description = "Convert Evernote XML Format (.enx) to the Netscape Bookmark File Format (bookmarks.html).") 115 | parser.add_argument('input', help = "Evernote .enx file to convert") 116 | parser.add_argument('output', help = "Output bookmarks.html file") 117 | parser.add_argument('--verbose', '-v', action='store_true', default=False) 118 | options = parser.parse_args() 119 | logging.basicConfig(level=logging.DEBUG if options.verbose else logging.INFO, format="%(message)s") 120 | convert(options.input, options.output) 121 | 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /tests/resources/missing_source_url_a_href.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Bookmarks 6 |

Bookmarks

7 |
8 |
Defining "Diaspora Jewry" 9 |
10 | -------------------------------------------------------------------------------- /tests/resources/missing_source_url_a_href.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Defining "Diaspora Jewry" 5 | 6 | 7 | 8 |
9 | 10 |

Defining "Diaspora Jewry"

11 | 12 |

http://hamodia.com/2016/04/17/defining-diaspora-jewry/

13 | 14 |

Reform clergyman Rick Jacobs, who serves as president of the Union for Reform Judaism, recently made a comment of interest to a secular Jewish newspaper. It was in response to chareidi opposition in Israel to a plan that would set aside an area adjacent to the Kosel Maaravi to accommodate unorthodox services. But his words were also aimed at the larger issue of religious standards in Israel for things like geirus, gittin v'kiddushin and the official recognition and government funding of Jewish movements that do not accept halachah as the arbiter of Jewish religious questions. What the Reform leader said was that, if Israel shuns compromise with the American Reform movement, "it will signal a serious rupture in the relationship between Diaspora Jewry and the Jewish state." And he went on to contend that "Reform Jewish leaders speak up every day on behalf of the State of Israel on the college campus and in our communities. We are asked to speak up for Israel, even as Israel treats Reform Judaism as inauthentic." If, by that latter statement, Jacobs means to imply that Reform Jews' support for Israel is contingent on the state's adoption of the Reform movement's religious matters determinations as an official counterpart to Israel's halachah-respecting standard, it is astonishingly revealing. Threatening to hold his constituents' support for Israel hostage until his movement is considered equal with the Judaism of the ages says much about the depth of Reform commitment to Israel's security...

15 | 16 |
17 | 18 |
19 | ]]>
20160418T044812Z
20 | -------------------------------------------------------------------------------- /tests/resources/missing_source_url_a_no_href.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Bookmarks 6 |

Bookmarks

7 |
8 |
Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphate 9 |
10 | -------------------------------------------------------------------------------- /tests/resources/missing_source_url_a_no_href.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphate 5 | 6 | 7 | 8 |
9 | 10 |

Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphate

11 | 12 |

http://www.dailymail.co.uk/news/article-3544016/Obama-plans-expanded-U-S-presence-Iraq-Syria-ahead-major-attacks-aimed-destroying-ISIS-s-called-Caliphate.html?ITO=1490&ns_mchannel=rss&ns_campaign=1490

13 | 14 |

The Obama administration is preparing to intensify its military operations against ISIS in both Iraq and Syria ahead of ground operations aimed at destroying the group's self-proclaimed Caliphate...

15 | 16 |
17 | 18 |
19 | ]]>
20160417T041217Z
20 | -------------------------------------------------------------------------------- /tests/resources/well_formed_note.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Bookmarks 6 |

Bookmarks

7 |
8 |
Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphate 9 |
10 | -------------------------------------------------------------------------------- /tests/resources/well_formed_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphatehttp://www.dailymail.co.uk/news/article-3544016/Obama-plans-expanded-U-S-presence-Iraq-Syria-ahead-major-attacks-aimed-destroying-ISIS-s-called-Caliphate.html 5 | 6 | 7 | 8 |
9 | 10 |

Obama plans expanded U.S. presence in Iraq and Syria ahead of major attacks aimed at destroying ISIS's so-called Caliphate

11 | 12 |

http://www.dailymail.co.uk/news/article-3544016/Obama-plans-expanded-U-S-presence-Iraq-Syria-ahead-major-attacks-aimed-destroying-ISIS-s-called-Caliphate.html?ITO=1490&ns_mchannel=rss&ns_campaign=1490

13 | 14 |

The Obama administration is preparing to intensify its military operations against ISIS in both Iraq and Syria ahead of ground operations aimed at destroying the group's self-proclaimed Caliphate...

15 | 16 |
17 | 18 |
19 | ]]>
20160417T041217Z
20 | -------------------------------------------------------------------------------- /tests/test-evernote-bookmarks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import os.path 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | import unittest 9 | 10 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 | TESTS_DIR = os.path.join(ROOT_DIR, "tests") 12 | RESOURCES_DIR = os.path.join(TESTS_DIR, "resources") 13 | 14 | EVERNOTE_BOOKMARKS_PATH = os.path.join(ROOT_DIR, "evernote-bookmarks") 15 | 16 | 17 | class TempDir(object): 18 | 19 | def __enter__(self): 20 | self.path = tempfile.mkdtemp() 21 | self.previous = os.getcwd() 22 | os.chdir(self.path) 23 | 24 | def __exit__(self, exc_type, exc_val, exc_tb): 25 | os.chdir(self.previous) 26 | shutil.rmtree(self.path) 27 | 28 | 29 | class TestEvernoteBookmarks(unittest.TestCase): 30 | 31 | def _assert_files_equal(self, path_a, path_b): 32 | with open(path_a, 'r') as a, open(path_b, 'r') as b: 33 | self.assertEqual(a.read(), b.read()) 34 | 35 | def _run_evernote_bookmarks_and_assert_results_expected(self, identifier): 36 | with TempDir(): 37 | subprocess.check_call([EVERNOTE_BOOKMARKS_PATH, 38 | os.path.join(RESOURCES_DIR, "%s.xml" % (identifier, )), 39 | "bookmarks.html"]) 40 | self._assert_files_equal("bookmarks.html", 41 | os.path.join(RESOURCES_DIR, "%s.html" % (identifier, ))) 42 | 43 | def test_well_formed_note(self): 44 | self._run_evernote_bookmarks_and_assert_results_expected("well_formed_note") 45 | 46 | def test_missing_source_url_a_href(self): 47 | self._run_evernote_bookmarks_and_assert_results_expected("missing_source_url_a_href") 48 | 49 | def test_missing_source_url_a_no_href(self): 50 | self._run_evernote_bookmarks_and_assert_results_expected("missing_source_url_a_no_href") 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | --------------------------------------------------------------------------------