Просмотр исходного кода

Finished image/drawing download (mimetypes too).
Added --header/--no-header flag.

Nathan Beals 5 лет назад
Родитель
Сommit
28b9d9996b
3 измененных файлов с 109 добавлено и 83 удалено
  1. 83 68
      keep_exporter/export.py
  2. 25 15
      poetry.lock
  3. 1 0
      pyproject.toml

+ 83 - 68
keep_exporter/export.py

@@ -1,49 +1,67 @@
 #!/usr/bin/env python3
-
-import os, pathlib
+import mimetypes
+import os
+import pathlib
 from typing import List
-import gkeepapi
-import frontmatter
+
 import click
-from pathvalidate import sanitize_filename
+import frontmatter
+import gkeepapi
 from mdutils.mdutils import MdUtils
-from tempfile import NamedTemporaryFile
+from pathvalidate import sanitize_filename
+
 
 def login(user_email: str, password: str) -> gkeepapi.Keep:
     keep = gkeepapi.Keep()
     try:
         keep.login(user_email, password)
-    except Exception:
-        raise click.BadParameter("Login failed.")
+    except gkeepapi.exception.LoginException as ex:
+        raise click.BadParameter(f"Login failed: {str(ex)}")
 
     return keep
 
-def download_images(keep: gkeepapi.Keep, note_count: int, note: gkeepapi._node.Note, outpath) -> List[pathlib.Path]:
-    if not note.images: return []
+
+def download_images(
+    keep: gkeepapi.Keep,
+    note_count: int,
+    note: gkeepapi._node.Note,
+    outpath: pathlib.Path,
+) -> List[pathlib.Path]:
+    if not note.images and not note.drawings:
+        return []
 
     ret = []
 
-    for image in note.images:
-        meta = image.blob.save()        
-        mimetype = meta['mimetype']
-        ocr = meta['extracted_text']
+    for image in note.images + note.drawings:
+        meta = image.blob.save()
+        # ocr = meta["extracted_text"]  # TODO save ocr as metadata? in markdown or image?
 
         url = keep._media_api.get(image)
         print("Downloading %s" % url)
 
+        if meta.get("type", "") == "DRAWING":
+            extension = mimetypes.guess_extension(
+                meta.get("drawingInfo", {})
+                .get("snapshotData", {})
+                .get("mimetype", "image/png")
+            )  # All drawings seem to be pngs
+        else:  # 'IMAGE'
+            extension = mimetypes.guess_extension(meta.get("mimetype", "image/jpeg"))
+
         imgfile = (
             outpath
-            / f'{sanitize_filename(f"{note_count:04} - " + image.server_id,max_len=135)}.jpg'
+            / f'{sanitize_filename(f"{note_count:04} - " + image.server_id,max_len=135)}{extension}'
         )
-        
+
         imgresp = keep._media_api._session.get(url)
-        with open(imgfile, 'wb') as f:
-            f.write( imgresp.content )
+        with imgfile.open("wb") as f:
+            f.write(imgresp.content)
 
-        ret.append( imgfile )
+        ret.append(imgfile)
 
     return ret
 
+
 def build_frontmatter(note: gkeepapi._node.Note, markdown: str) -> frontmatter.Post:
     metadata = {
         "id": note.id,
@@ -63,7 +81,7 @@ def build_frontmatter(note: gkeepapi._node.Note, markdown: str) -> frontmatter.P
         },
     }
 
-    # gkeepapi appears to be treating "0" as a timestamp rather than null
+    # gkeepapi appears to be treating "0" as a timestamp rather than null. Sometimes the data structure does not have the key at all instead of 0.
     if note.timestamps.trashed and note.timestamps.trashed.year > 1970:
         metadata["timestamps"]["trashed"] = note.timestamps.trashed.timestamp()
     if note.timestamps.deleted and note.timestamps.deleted.year > 1970:
@@ -71,47 +89,41 @@ def build_frontmatter(note: gkeepapi._node.Note, markdown: str) -> frontmatter.P
 
     return frontmatter.Post(markdown, handler=None, **metadata)
 
+
 def build_markdown(note: gkeepapi._node.Note, images: List[pathlib.Path]) -> str:
-    # mdutils demands a filename to write to
-    # and doesn't seem to support working in memory
-    with NamedTemporaryFile() as f:
-        doc = MdUtils(file_name=f.name)
-
-        doc.new_header(1, note.title)
-        doc.new_header(2, "Note")
-
-        text = note.text
-        text = text.replace('☑ ', '- [X] ')
-        text = text.replace('☐ ', '- [ ] ')
-
-        doc.new_paragraph(text)
-
-        if note.annotations.links:
-            doc.new_line()
-            doc.new_line()
-            doc.new_header(2, "Links")
-            doc.new_list(
-                [
-                    doc.new_inline_link(link=link.url, text=link.title)
-                    for link in note.annotations.links
-                ]
-            )
-
-        if images:
-            doc.new_header(2, "Attached Images")
-            doc.new_line()
-            doc.new_line()
-
-            for image in images:
-                doc.new_line( doc.new_inline_image("", image.name) )
-
-        # doc.create_md_file()
-        # create_md_file writes out:
-        #    data=self.title + self.table_of_contents + self.file_data_text + self.reference.get_references_as_markdown()
-        # but we only care about file_data_text
-        # since we're not generating a TOC or references
-        # and the above adds unnecessary newlines and reading from disk
-        return doc.file_data_text
+    doc = MdUtils(
+        ""
+    )  # mdutils requires a string file name. Since we're not using it to write files, we can ignore that.
+
+    doc.new_header(1, note.title)
+    doc.new_header(2, "Note")
+
+    text = note.text
+    text = text.replace("☑ ", "- [X] ")
+    text = text.replace("☐ ", "- [ ] ")
+
+    doc.new_paragraph(text)
+
+    if note.annotations.links:
+        doc.new_line()
+        doc.new_line()
+        doc.new_header(2, "Links")
+        doc.new_list(
+            [
+                doc.new_inline_link(link=link.url, text=link.title)
+                for link in note.annotations.links
+            ]
+        )
+
+    if images:
+        doc.new_line()
+        doc.new_header(2, "Attached Images")
+
+        for image in images:
+            doc.new_line(doc.new_inline_image("", image.name))
+
+    return doc.file_data_text
+
 
 @click.command()
 @click.option(
@@ -135,7 +147,8 @@ def build_markdown(note: gkeepapi._node.Note, images: List[pathlib.Path]) -> str
     help="Google account password (environment variable 'GKEEP_PASSWORD')",
     hide_input=True,
 )
-def main(directory, user, password):
+@click.option("--header/--no-header", default=True)
+def main(directory, user, password, header):
     """A simple utility to export google keep notes to markdown files with metadata stored as a frontmatter header."""
     outpath = pathlib.Path(directory).resolve()
 
@@ -144,14 +157,14 @@ def main(directory, user, password):
     keep = login(user, password)
 
     click.echo("Beginning note export.")
-    
+
     if not outpath.exists():
         click.echo("output directory does not exist, creating.")
         outpath.mkdir(parents=True)
 
     notes: List[gkeepapi.node.TopLevelNode] = keep.all()
     note_count = 0
-    for note in notes: # type: gkeepapi.node.TopLevelNode
+    for note in notes:  # type: gkeepapi.node.TopLevelNode
         note_count += 1
         click.echo(f"Processing note #{note_count}")
 
@@ -166,14 +179,16 @@ def main(directory, user, password):
 
         images = download_images(keep, note_count, note, outpath)
         markdown = build_markdown(note, images)
-        fmatter = build_frontmatter(note, markdown)
-
 
-        with open(outfile, "wb+") as f:
-            frontmatter.dump(fmatter, f)
+        with outfile.open("wb+") as f:
+            if header:
+                fmatter = build_frontmatter(note, markdown)
+                frontmatter.dump(fmatter, f)
+            else:
+                f.write(markdown.encode("utf-8"))
 
     click.echo("Done.")
 
 
 if __name__ == "__main__":
-    main()
+    main()

+ 25 - 15
poetry.lock

@@ -214,14 +214,13 @@ python-versions = "*"
 PyYAML = "*"
 six = "*"
 
-
 [[package]]
 name = "pyyaml"
-version = "5.3.1"
+version = "5.4.1"
 description = "YAML parser and emitter for Python"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [[package]]
 name = "regex"
@@ -305,7 +304,7 @@ python-versions = "*"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "608ac5f32f9eb09115d688a49cfd1ae71f81f09248caae13069940b9b7310f95"
+content-hash = "1aa17b3ad74b68d634b2e52a41677a4a34b494b7e69ff477f4d4d7b9a376781d"
 
 [metadata.files]
 appdirs = [
@@ -317,6 +316,7 @@ astroid = [
     {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
 ]
 black = [
+    {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"},
     {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
 ]
 certifi = [
@@ -439,17 +439,27 @@ python-frontmatter = [
     {file = "python_frontmatter-0.5.0-py3-none-any.whl", hash = "sha256:a7dcdfdaf498d488dce98bfa9452f8b70f803a923760ceab1ebd99291d98d28a"},
 ]
 pyyaml = [
-    {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
-    {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
-    {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"},
-    {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"},
-    {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"},
-    {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"},
-    {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"},
-    {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"},
-    {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"},
-    {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
-    {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
+    {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
+    {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
+    {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
+    {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
 ]
 regex = [
     {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},

+ 1 - 0
pyproject.toml

@@ -20,6 +20,7 @@ mdutils = "^1.3.0"
 [tool.poetry.dev-dependencies]
 black = {version = "^20.8b1", allow-prereleases = true}
 pylint = "^2.6.0"
+isort = "^5.7.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]