Nathan Beals 4 лет назад
Родитель
Сommit
6dd5827ff2
2 измененных файлов с 31 добавлено и 172 удалено
  1. 2 6
      keep_exporter/cli.py
  2. 29 166
      keep_exporter/export.py

+ 2 - 6
keep_exporter/cli.py

@@ -13,6 +13,7 @@ from keep_exporter.export import (
     build_frontmatter,
     build_markdown,
     build_note_unique_path,
+    write_note,
     delete_local_only_files,
     download_media,
     index_existing_files,
@@ -255,12 +256,7 @@ def main(
 
         downloaded_media += downloaded
 
-        with target_path.open("wb+") as f:
-            if header:
-                fmatter = build_frontmatter(note, markdown)
-                frontmatter.dump(fmatter, f)
-            else:
-                f.write(markdown.encode("utf-8"))
+        write_note(target_path, header, note, markdown)
 
     click.echo("Finished syncing.")
     click.echo(

+ 29 - 166
keep_exporter/export.py

@@ -1,12 +1,13 @@
 #!/usr/bin/env python3
+# pylint: disable=protected-access
 import datetime
 import mimetypes
 import pathlib
-from typing import (Dict, List, NamedTuple, Optional, Set, Tuple, Union,
-                    ValuesView)
+from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union, ValuesView
 
 import click
-import click_config_file
+
+# import click_config_file
 import frontmatter
 import gkeepapi
 from gkeepapi.node import NodeAudio, NodeDrawing, NodeImage
@@ -15,6 +16,7 @@ from pathvalidate import sanitize_filename
 
 mimetypes.add_type("audio/3gpp", ".3gp")
 
+
 def all_note_media(
     note: gkeepapi._node.Note,
 ) -> List[Union[NodeImage, NodeDrawing, NodeAudio]]:
@@ -25,17 +27,12 @@ def all_note_media(
     """
     return note.images + note.drawings + note.audio
 
-
-def all_note_media( note: gkeepapi._node.Note ) -> List[Union[NodeImage, NodeDrawing, NodeAudio]]:
-    """
-    Returns a filtered list of only "media" blobs associate with the note.
-    Currently NodeDrawing, NodeImage, and NodeMedia.
-    There are other blob types, but they don't seem actionable as media.
-    """
-    return note.images + note.drawings + note.audio
-
-
-def download_media( keep: gkeepapi.Keep, note: gkeepapi._node.Note, mediapath: pathlib.Path, skip_existing: bool) -> Tuple[List[pathlib.Path], int]:
+def download_media(
+    keep: gkeepapi.Keep,
+    note: gkeepapi._node.Note,
+    mediapath: pathlib.Path,
+    skip_existing: bool,
+) -> Tuple[List[pathlib.Path], int]:
 
     note_media = all_note_media(note)
     if not note_media:
@@ -88,8 +85,8 @@ def download_media( keep: gkeepapi.Keep, note: gkeepapi._node.Note, mediapath: p
 
             url = keep._media_api.get(media)
             media_data = keep._media_api._session.get(url)
-            with media_file.open("wb") as f:
-                f.write(media_data.content)
+            with media_file.open("wb") as file_handle:
+                file_handle.write(media_data.content)
 
             downloaded_media += 1
 
@@ -118,7 +115,8 @@ def build_frontmatter(note: gkeepapi._node.Note, markdown: str) -> frontmatter.P
         },
     }
 
-    # 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.
+    # 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:
@@ -130,7 +128,7 @@ def build_frontmatter(note: gkeepapi._node.Note, markdown: str) -> frontmatter.P
 def build_markdown(note: gkeepapi._node.Note, images: List[pathlib.Path]) -> str:
     doc = MdUtils(
         ""
-    )  # mdutils requires a string file name. Since we're not using it to write files, we can ignore that.
+    )  # mdutils requires a string file name. We're not using it to write files, ignore it.
 
     doc.new_header(1, note.title)
     doc.new_header(2, "Note")
@@ -161,6 +159,15 @@ def build_markdown(note: gkeepapi._node.Note, images: List[pathlib.Path]) -> str
 
     return doc.file_data_text
 
+def write_note(target_path, header, note, markdown):
+    """ Writes built notes to disk, optionally builds a header/frontmatter too."""
+    with target_path.open("wb+") as file_handle:
+        if header:
+            front_matter = build_frontmatter(note, markdown)
+            frontmatter.dump(front_matter, file_handle)
+        else:
+            file_handle.write(markdown.encode("utf-8"))
+
 
 LocalMedia = NamedTuple(
     "LocalMedia",
@@ -211,8 +218,8 @@ def index_existing_files(directory: pathlib.Path) -> Dict[str, LocalNote]:
         # markdown file
         if file.name.endswith(".md"):
             try:
-                with open(file, "r") as f:
-                    fm = frontmatter.load(f)
+                with open(file, "r") as file_handle:
+                    fm = frontmatter.load(file_handle)
 
                     google_keep_id: str = fm.metadata.get("google_keep_id")
                     if google_keep_id:
@@ -239,7 +246,7 @@ def index_existing_files(directory: pathlib.Path) -> Dict[str, LocalNote]:
             except IOError as ex:
                 errors = 0
                 click.echo(
-                    "Unable to open Markdown file [{os.path.join(root, file)}]. Skipping: {str(ex)}",
+                    f"Unable to open Markdown file {file}. Skipping: {str(ex)}",
                     err=True,
                 )
 
@@ -277,7 +284,7 @@ def try_rename_note(note: LocalNote, target_file: pathlib.Path) -> pathlib.Path:
         note.path.rename(target_file)
         return target_file
     except Exception as ex:
-        click.echo(f"Unable to rename note. Using existing name: %s" % ex, err=True)
+        click.echo(f"Unable to rename note. Using existing name: {ex}", err=True)
         return note.path
 
 
@@ -392,147 +399,3 @@ def delete_local_only_files(
         deleted_media += 1
 
     return (deleted_notes, deleted_media)
-
-
-@click.command(
-    context_settings={"max_content_width": 120, "help_option_names": ["-h", "--help"]}
-)
-@click.option(
-    "--user",
-    "-u",
-    prompt=True,
-    required=True,
-    envvar="GKEEP_USER",
-    show_envvar=True,
-    help="Google account email (prompt if empty)",
-)
-@click.option(
-    "--password",
-    "-p",
-    prompt=True,
-    required=True,
-    envvar="GKEEP_PASSWORD",
-    show_envvar=True,
-    help="Google account password (prompt if empty)",
-    hide_input=True,
-)
-@click.option(
-    "--directory",
-    "-d",
-    default="./gkeep-export",
-    show_default=True,
-    help="Output directory for exported notes",
-    type=click.Path(file_okay=False, dir_okay=True, writable=True),
-)
-@click.option(
-    "--header/--no-header",
-    default=True,
-    show_default=True,
-    help="Choose to include or exclude the frontmatter header",
-)
-@click.option(
-    "--delete-local/--no-delete-local",
-    default=False,
-    show_default=True,
-    help="Choose to delete or leave as-is any notes that exist locally but not in Google Keep",
-)
-@click.option(
-    "--rename-local/--no-rename-local",
-    default=False,
-    show_default=True,
-    help="Choose to rename or leave as-is any notes that change titles in Google Keep",
-)
-@click.option(
-    "--date-format",
-    default="%Y-%m-%d",
-    show_default=True,
-    help="Date format to use for the prefix of the note filenames. Reflects the created date of the note.",
-)
-@click.option(
-    "--skip-existing-media/--no-skip-existing-media",
-    default=True,
-    show_default=True,
-    help="Skip existing media if it appears unchanged from the local copy.",
-)
-def main(
-    directory: str,
-    user: str,
-    password: str,
-    header: bool,
-    delete_local: bool,
-    rename_local: bool,
-    date_format: str,
-    skip_existing_media: bool,
-):
-    """A simple utility to export google keep notes to markdown files with metadata stored as a frontmatter header."""
-    notepath = pathlib.Path(directory).resolve()
-    mediapath = notepath.joinpath("media/")
-
-    click.echo(f"Notes directory: {notepath}")
-    click.echo(f"Media directory: {mediapath}")
-
-    click.echo("Logging in.")
-    keep = login(user, password)
-
-    if not notepath.exists():
-        click.echo("Notes directory does not exist, creating.")
-        notepath.mkdir(parents=True)
-
-    if not mediapath.exists():
-        click.echo("Media directory does not exist, creating.")
-        mediapath.mkdir(parents=True)
-
-    click.echo("Indexing local files.")
-    local_index = index_existing_files(notepath)
-
-    click.echo("Indexing remote notes.")
-    keep_notes = dict([(note.id, note) for note in keep.all()])
-
-    skipped_notes, updated_notes, new_notes = 0, 0, 0
-    downloaded_media = 0
-    deleted_notes, deleted_media = delete_local_only_files(
-        local_index, keep_notes, delete_local
-    )
-
-    for note in keep_notes.values():  # type: gkeepapi._node.Note
-        local_note = local_index.get(note.id)
-        if not local_note:
-            click.echo(f"Downloading new note {note.id}")
-            new_notes += 1
-
-        target_path = build_note_unique_path(notepath, note, date_format, local_index)
-
-        local_path = local_index.get(note.id, LocalNote(note.id)).path
-        if local_path:
-            if rename_local and local_path != target_path:
-                target_path = try_rename_note(local_index[note.id], target_path)
-            else:
-                target_path = local_path
-
-        # decide to skip after the rename (due to date format change) has a chance
-        if local_note:
-            if local_note.timestamp_updated == note.timestamps.updated:
-                skipped_notes += 1
-                continue
-            else:
-                updated_notes += 1
-                click.echo(f"Updating existing file for note {note.id}")
-
-
-        images, downloaded = download_media(keep, note, mediapath, skip_existing_media)
-        markdown = build_markdown(note, images)
-
-        downloaded_media += downloaded
-
-        with target_path.open("wb+") as f:
-            if header:
-                fmatter = build_frontmatter(note, markdown)
-                frontmatter.dump(fmatter, f)
-            else:
-                f.write(markdown.encode("utf-8"))
-
-    click.echo("Finished syncing.")
-    click.echo(
-        f"Notes: {skipped_notes} unchanged, {updated_notes} updated, {new_notes} new, {deleted_notes} deleted"
-    )
-    click.echo(f"Media: {downloaded_media} downloaded, {deleted_media} deleted")