4
0
Эх сурвалжийг харах

Merge branch 'delete_rename_local_index' of https://github.com/mbafford/keep-exporter into mbafford-delete_rename_local_index

Nathan Beals 4 жил өмнө
parent
commit
1405e84bea
1 өөрчлөгдсөн 157 нэмэгдсэн , 8 устгасан
  1. 157 8
      keep_exporter/export.py

+ 157 - 8
keep_exporter/export.py

@@ -2,7 +2,8 @@
 import datetime
 import mimetypes
 import pathlib
-from typing import Any, 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
@@ -25,12 +26,16 @@ def all_note_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 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]:
 
     note_media = all_note_media(note)
     if not note_media:
@@ -234,7 +239,7 @@ def index_existing_files(directory: pathlib.Path) -> Dict[str, LocalNote]:
             except IOError as ex:
                 errors = 0
                 click.echo(
-                    f"Unable to open Markdown file [{str(file.resolve())}]. Skipping: {str(ex)}",
+                    "Unable to open Markdown file [{os.path.join(root, file)}]. Skipping: {str(ex)}",
                     err=True,
                 )
 
@@ -387,3 +392,147 @@ 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")