remarkablesync 2.0.0__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remarkablesync-2.0.0/remarkablesync.egg-info → remarkablesync-2.0.2}/PKG-INFO +2 -2
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/README.md +1 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2/remarkablesync.egg-info}/PKG-INFO +2 -2
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/remarkablesync.egg-info/SOURCES.txt +1 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/__version__.py +1 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/backup/backup_manager.py +4 -30
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/backup_command.py +2 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/config_command.py +34 -24
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/pipeline.py +28 -21
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/sync_command.py +2 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/watch_command.py +2 -2
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/config.py +11 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/hybrid_converter.py +117 -67
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/pdf_md_converter.py +113 -99
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/rm_pdf_converter.py +35 -38
- remarkablesync-2.0.2/src/utils/__init__.py +33 -0
- remarkablesync-2.0.2/src/utils/name_registry.py +117 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_config.py +2 -2
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_hybrid_converter.py +2 -2
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_md_exporter.py +16 -14
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_pipeline.py +4 -4
- remarkablesync-2.0.0/src/utils/__init__.py +0 -1
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/LICENSE +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/MANIFEST.in +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/RemarkableSync.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/pyproject.toml +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/remarkablesync.egg-info/dependency_links.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/remarkablesync.egg-info/entry_points.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/remarkablesync.egg-info/requires.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/remarkablesync.egg-info/top_level.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/requirements-dev.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/requirements.txt +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/setup.cfg +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/setup.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ai/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ai/base_provider.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ai/claude_provider.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ai/github_models_provider.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/auth/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/auth/github_device_flow.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/backup/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/backup/connection.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/backup/metadata.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/commands/convert_command.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/converters/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/converters/base_converter.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/converters/v4_converter.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/converters/v5_converter.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/converters/v6_converter.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/keyring_store.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/md_export/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ocr/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/ocr/ocr_engine.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/template_renderer.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/utils/console.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/src/utils/logging.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/__init__.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/conftest.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/generate_test_assets.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/mock_ai_provider.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/mock_connection.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_ai_providers.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_auth_device_flow.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_basic.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_console.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_keyring_store.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_logging.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_metadata.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_ocr_pipeline.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_template_renderer.py +0 -0
- {remarkablesync-2.0.0 → remarkablesync-2.0.2}/tests/test_wifi_connection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remarkablesync
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Backup and convert reMarkable tablet notebooks to PDF
|
|
5
5
|
Home-page: https://github.com/JeffSteinbok/RemarkableSync
|
|
6
6
|
Author: Jeff Steinbok
|
|
@@ -54,7 +54,7 @@ Dynamic: summary
|
|
|
54
54
|
|
|
55
55
|
# RemarkableSync
|
|
56
56
|
|
|
57
|
-

|
|
57
|
+

|
|
58
58
|
|
|
59
59
|
[](https://github.com/JeffSteinbok/RemarkableSync)
|
|
60
60
|
[](https://github.com/JeffSteinbok/RemarkableSync/releases)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RemarkableSync
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
5
|
[](https://github.com/JeffSteinbok/RemarkableSync)
|
|
6
6
|
[](https://github.com/JeffSteinbok/RemarkableSync/releases)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remarkablesync
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Backup and convert reMarkable tablet notebooks to PDF
|
|
5
5
|
Home-page: https://github.com/JeffSteinbok/RemarkableSync
|
|
6
6
|
Author: Jeff Steinbok
|
|
@@ -54,7 +54,7 @@ Dynamic: summary
|
|
|
54
54
|
|
|
55
55
|
# RemarkableSync
|
|
56
56
|
|
|
57
|
-

|
|
57
|
+

|
|
58
58
|
|
|
59
59
|
[](https://github.com/JeffSteinbok/RemarkableSync)
|
|
60
60
|
[](https://github.com/JeffSteinbok/RemarkableSync/releases)
|
|
@@ -298,7 +298,7 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
298
298
|
progress.update(task, advance=1, description=local_path.name[:40])
|
|
299
299
|
|
|
300
300
|
except (OSError, SCPException) as e:
|
|
301
|
-
print_error(f"
|
|
301
|
+
print_error(f" ERR - Failed to download {remote_file['path']}: {e}")
|
|
302
302
|
progress.update(task, advance=1)
|
|
303
303
|
|
|
304
304
|
# Save metadata
|
|
@@ -373,7 +373,7 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
373
373
|
self.metadata.update_file_metadata(remote_file, local_path)
|
|
374
374
|
|
|
375
375
|
except (OSError, SCPException) as e:
|
|
376
|
-
print_error(f"
|
|
376
|
+
print_error(f" ERR - Failed to download {remote_file['path']}: {e}")
|
|
377
377
|
|
|
378
378
|
progress.update(task, advance=1, description=local_path.name[:40])
|
|
379
379
|
|
|
@@ -527,34 +527,15 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
527
527
|
output_dir = self.backup_dir / "PDF"
|
|
528
528
|
logging.warning("No pdf_dir configured, falling back to %s", output_dir)
|
|
529
529
|
|
|
530
|
-
|
|
531
|
-
updated_only_file = None
|
|
532
|
-
if force_convert_all:
|
|
533
|
-
logging.info("Force conversion enabled - converting all notebooks to PDF")
|
|
534
|
-
elif updated_notebook_uuids:
|
|
535
|
-
# Create a temporary file list of updated notebooks for selective conversion
|
|
536
|
-
updated_list_file = self.backup_dir / "updated_notebooks.txt"
|
|
537
|
-
try:
|
|
538
|
-
with open(updated_list_file, "w", encoding="utf-8") as f:
|
|
539
|
-
for uuid in sorted(updated_notebook_uuids):
|
|
540
|
-
f.write(f"{uuid}\n")
|
|
541
|
-
|
|
542
|
-
updated_only_file = updated_list_file
|
|
543
|
-
logging.info("Converting %d updated notebooks to PDF", len(updated_notebook_uuids))
|
|
544
|
-
except OSError as e:
|
|
545
|
-
logging.error("Failed to create updated notebooks list: %s", e)
|
|
546
|
-
return False
|
|
547
|
-
else:
|
|
530
|
+
if not updated_notebook_uuids and not force_convert_all:
|
|
548
531
|
logging.info("No notebooks were updated - skipping PDF conversion")
|
|
549
532
|
return True
|
|
550
533
|
|
|
551
|
-
# Load folder filter from config
|
|
552
534
|
from ..config import load_config
|
|
553
535
|
|
|
554
536
|
config = load_config()
|
|
555
537
|
folder_filter = config.get("folders", []) or None
|
|
556
538
|
|
|
557
|
-
# Run conversion
|
|
558
539
|
try:
|
|
559
540
|
success, _converted = run_conversion(
|
|
560
541
|
backup_dir=self.backup_dir,
|
|
@@ -562,18 +543,11 @@ class ReMarkableBackup: # pylint: disable=too-many-instance-attributes
|
|
|
562
543
|
verbose="INF",
|
|
563
544
|
sample=None,
|
|
564
545
|
notebook_filter=None,
|
|
565
|
-
|
|
546
|
+
updated_uuids=updated_notebook_uuids if not force_convert_all else None,
|
|
566
547
|
updated_pages=updated_pages,
|
|
567
548
|
folder_filter=folder_filter,
|
|
568
549
|
)
|
|
569
550
|
|
|
570
|
-
# Clean up temporary file if created
|
|
571
|
-
if updated_only_file and updated_only_file.exists():
|
|
572
|
-
try:
|
|
573
|
-
updated_only_file.unlink()
|
|
574
|
-
except OSError:
|
|
575
|
-
pass # Ignore cleanup errors
|
|
576
|
-
|
|
577
551
|
if success:
|
|
578
552
|
logging.info("PDF conversion completed successfully")
|
|
579
553
|
else:
|
|
@@ -34,7 +34,8 @@ def run_backup_command(
|
|
|
34
34
|
Returns:
|
|
35
35
|
Exit code (0 for success, 1 for failure)
|
|
36
36
|
"""
|
|
37
|
-
|
|
37
|
+
log_dir = backup_dir.parent
|
|
38
|
+
setup_logging(log_level, log_dir=log_dir)
|
|
38
39
|
|
|
39
40
|
print("ReMarkable Tablet Backup")
|
|
40
41
|
print("=" * 70)
|
|
@@ -102,13 +102,11 @@ def run_config_command() -> int:
|
|
|
102
102
|
click.echo(" 5. Re-run this wizard with the IP ready")
|
|
103
103
|
return 1
|
|
104
104
|
|
|
105
|
-
# Let user confirm/change the IP (pre-filled from device or config)
|
|
106
|
-
default_ip = wifi_host or current.get("wifi_host", "") or "
|
|
105
|
+
# Let user confirm/change the IP (pre-filled from device or config; blank if unknown)
|
|
106
|
+
default_ip = wifi_host or current.get("wifi_host", "") or ""
|
|
107
107
|
wifi_host = inquirer.text(
|
|
108
108
|
message="Tablet WiFi IP address:",
|
|
109
109
|
default=default_ip,
|
|
110
|
-
validate=lambda x: len(x.strip()) > 0,
|
|
111
|
-
invalid_message="IP address cannot be empty.",
|
|
112
110
|
).execute()
|
|
113
111
|
|
|
114
112
|
if wifi_host is None:
|
|
@@ -163,24 +161,36 @@ def run_config_command() -> int:
|
|
|
163
161
|
backup_dir = _default_backup_dir()
|
|
164
162
|
click.echo(f" Using default: {backup_dir}")
|
|
165
163
|
|
|
166
|
-
# 5. Sync actions
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
# 5. Sync actions — later steps imply earlier ones (backup → pdf → ocr)
|
|
165
|
+
action_order = [value for value, _ in SYNC_ACTIONS]
|
|
166
|
+
current_actions = current.get("sync_actions", ["backup", "pdf", "ocr"])
|
|
167
|
+
if not current_actions:
|
|
168
|
+
current_actions = action_order
|
|
169
|
+
|
|
170
|
+
# Build cascade choices: each option enables all steps up to and including it
|
|
171
|
+
cascade_labels = {
|
|
172
|
+
"backup": "Backup only",
|
|
173
|
+
"pdf": "Backup + PDF Conversion",
|
|
174
|
+
"ocr": "Backup + PDF Conversion + AI OCR & Markdown Export",
|
|
175
|
+
}
|
|
176
|
+
highest_current = max(
|
|
177
|
+
(action_order.index(a) for a in current_actions if a in action_order), default=0
|
|
178
|
+
)
|
|
179
|
+
default_action = action_order[highest_current]
|
|
172
180
|
|
|
173
|
-
|
|
181
|
+
chosen = inquirer.select(
|
|
174
182
|
message="What to do on sync:",
|
|
175
|
-
choices=
|
|
176
|
-
|
|
177
|
-
invalid_message="Select at least one sync action.",
|
|
183
|
+
choices=[{"name": cascade_labels[value], "value": value} for value, _ in SYNC_ACTIONS],
|
|
184
|
+
default=default_action,
|
|
178
185
|
).execute()
|
|
179
186
|
|
|
180
|
-
if
|
|
187
|
+
if chosen is None:
|
|
181
188
|
click.echo("Configuration cancelled.")
|
|
182
189
|
return 0
|
|
183
190
|
|
|
191
|
+
# Cascade: all steps up to and including the chosen step
|
|
192
|
+
sync_actions = action_order[: action_order.index(chosen) + 1]
|
|
193
|
+
|
|
184
194
|
# 6. PDF output directory (if PDF or OCR selected)
|
|
185
195
|
from src.config import _default_documents_dir
|
|
186
196
|
|
|
@@ -412,7 +422,7 @@ def run_config_command() -> int:
|
|
|
412
422
|
click.echo(f" Images: {'yes (_images/ folder)' if embed_images else 'no'}")
|
|
413
423
|
click.echo(f" AI: {ai_provider} ({ai_model})")
|
|
414
424
|
has_token = bool(github_token or claude_api_key)
|
|
415
|
-
click.echo(f" Token: {'
|
|
425
|
+
click.echo(f" Token: {'OK - saved in keyring' if has_token else '(not set)'}")
|
|
416
426
|
click.echo()
|
|
417
427
|
|
|
418
428
|
return 0
|
|
@@ -456,14 +466,14 @@ def _enable_wifi_ssh(password: str) -> str:
|
|
|
456
466
|
try:
|
|
457
467
|
from src.backup.connection import USB_HOST, ReMarkableConnection
|
|
458
468
|
except ImportError:
|
|
459
|
-
click.echo("
|
|
469
|
+
click.echo(" WRN - Could not import connection module.")
|
|
460
470
|
return ""
|
|
461
471
|
|
|
462
472
|
conn = ReMarkableConnection(password=password, host=USB_HOST)
|
|
463
473
|
click.echo(" Connecting via USB...")
|
|
464
474
|
|
|
465
475
|
if not conn.connect():
|
|
466
|
-
click.echo("
|
|
476
|
+
click.echo(" WRN - Could not connect via USB. Is the tablet plugged in?")
|
|
467
477
|
return ""
|
|
468
478
|
|
|
469
479
|
try:
|
|
@@ -471,7 +481,7 @@ def _enable_wifi_ssh(password: str) -> str:
|
|
|
471
481
|
click.echo(" Enabling WiFi SSH...")
|
|
472
482
|
stdout, stderr, exit_code = conn.execute_command("rm-ssh-over-wlan on")
|
|
473
483
|
if exit_code != 0:
|
|
474
|
-
click.echo(f"
|
|
484
|
+
click.echo(f" WRN - Command failed: {stderr.strip() or stdout.strip()}")
|
|
475
485
|
return ""
|
|
476
486
|
|
|
477
487
|
click.echo(" WiFi SSH enabled!")
|
|
@@ -487,11 +497,11 @@ def _enable_wifi_ssh(password: str) -> str:
|
|
|
487
497
|
click.echo(f" Tablet WiFi IP: {ip}")
|
|
488
498
|
return ip
|
|
489
499
|
|
|
490
|
-
click.echo("
|
|
500
|
+
click.echo(" WRN - Could not determine WiFi IP. Is the tablet on WiFi?")
|
|
491
501
|
return ""
|
|
492
502
|
|
|
493
503
|
except Exception as exc:
|
|
494
|
-
click.echo(f"
|
|
504
|
+
click.echo(f" WRN - Error enabling WiFi SSH: {exc}")
|
|
495
505
|
return ""
|
|
496
506
|
finally:
|
|
497
507
|
conn.disconnect()
|
|
@@ -522,7 +532,7 @@ def _get_folder_choices_live(
|
|
|
522
532
|
)
|
|
523
533
|
|
|
524
534
|
if not conn.connect():
|
|
525
|
-
click.echo("
|
|
535
|
+
click.echo(" WRN - Could not connect to tablet.")
|
|
526
536
|
return []
|
|
527
537
|
|
|
528
538
|
try:
|
|
@@ -536,7 +546,7 @@ def _get_folder_choices_live(
|
|
|
536
546
|
f"done"
|
|
537
547
|
)
|
|
538
548
|
if exit_code != 0:
|
|
539
|
-
click.echo("
|
|
549
|
+
click.echo(" WRN - Failed to read metadata from tablet.")
|
|
540
550
|
return []
|
|
541
551
|
|
|
542
552
|
# Parse the output — each metadata block starts with FILE: line
|
|
@@ -566,7 +576,7 @@ def _get_folder_choices_live(
|
|
|
566
576
|
|
|
567
577
|
except Exception as exc:
|
|
568
578
|
logging.debug("Failed to list folders from tablet: %s", exc)
|
|
569
|
-
click.echo(f"
|
|
579
|
+
click.echo(f" WRN - Error reading folders: {exc}")
|
|
570
580
|
return []
|
|
571
581
|
finally:
|
|
572
582
|
conn.disconnect()
|
|
@@ -14,6 +14,7 @@ from typing import Dict, List, Optional
|
|
|
14
14
|
from ..backup import ReMarkableBackup
|
|
15
15
|
from ..backup.connection import USB_HOST
|
|
16
16
|
from ..rm_pdf_converter import run_conversion
|
|
17
|
+
from ..utils import write_manifest
|
|
17
18
|
from ..utils.console import print_error, print_success, print_warn
|
|
18
19
|
from ..utils.logging import setup_logging
|
|
19
20
|
|
|
@@ -67,7 +68,8 @@ def run_pipeline(
|
|
|
67
68
|
"""
|
|
68
69
|
import time as _time
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
log_dir = backup_dir.parent
|
|
72
|
+
setup_logging(log_level, log_dir=log_dir)
|
|
71
73
|
_start_time = _time.monotonic()
|
|
72
74
|
|
|
73
75
|
from ..config import load_config
|
|
@@ -115,13 +117,18 @@ def run_pipeline(
|
|
|
115
117
|
try:
|
|
116
118
|
success, updated_uuids, updated_pages = backup_tool.backup_files()
|
|
117
119
|
if not success:
|
|
118
|
-
print_error("
|
|
120
|
+
print_error(" ERR - Backup failed.")
|
|
119
121
|
return 1
|
|
120
122
|
backup_tool.backup_templates()
|
|
121
|
-
print_success(f"
|
|
123
|
+
print_success(f" OK - Backed up ({len(updated_uuids)} notebooks updated)")
|
|
124
|
+
write_manifest(
|
|
125
|
+
backup_dir.parent / "updated_notebooks.txt",
|
|
126
|
+
sorted(updated_uuids),
|
|
127
|
+
"updated_notebooks",
|
|
128
|
+
)
|
|
122
129
|
except Exception as exc: # noqa: BLE001
|
|
123
130
|
logging.error("Backup error: %s", exc)
|
|
124
|
-
print_error(f"
|
|
131
|
+
print_error(f" ERR - Backup failed: {exc}")
|
|
125
132
|
return 1
|
|
126
133
|
else:
|
|
127
134
|
print("\n[1/3] Backup skipped (--skip-backup)")
|
|
@@ -133,29 +140,27 @@ def run_pipeline(
|
|
|
133
140
|
|
|
134
141
|
if not skip_convert:
|
|
135
142
|
print("\n[2/3] Converting notebooks to PDF...")
|
|
136
|
-
updated_list_file: Optional[Path] = None
|
|
137
|
-
|
|
138
|
-
if not force_convert and updated_uuids is not None and not skip_backup:
|
|
139
|
-
updated_list_file = backup_dir / "updated_notebooks.txt"
|
|
140
|
-
try:
|
|
141
|
-
updated_list_file.write_text("\n".join(sorted(updated_uuids)), encoding="utf-8")
|
|
142
|
-
except OSError as exc:
|
|
143
|
-
logging.warning("Could not write updated notebooks list: %s", exc)
|
|
144
|
-
updated_list_file = None
|
|
145
143
|
|
|
146
144
|
try:
|
|
147
|
-
_ok, converted_pages = run_conversion(
|
|
145
|
+
_ok, converted_pages, merged_pdfs = run_conversion(
|
|
148
146
|
backup_dir=backup_dir,
|
|
149
147
|
output_dir=pdf_output_dir,
|
|
150
148
|
verbose=log_level,
|
|
151
|
-
|
|
149
|
+
updated_uuids=updated_uuids if not force_convert and not skip_backup else None,
|
|
152
150
|
updated_pages=updated_pages,
|
|
153
151
|
folder_filter=folder_filter,
|
|
154
152
|
)
|
|
155
|
-
print_success("
|
|
153
|
+
print_success(" OK - PDF conversion done")
|
|
154
|
+
all_page_pdfs = sorted(p for pages in converted_pages.values() for p in pages)
|
|
155
|
+
write_manifest(
|
|
156
|
+
backup_dir.parent / "updated_pdf_pages.txt", all_page_pdfs, "updated_pdf_pages"
|
|
157
|
+
)
|
|
158
|
+
write_manifest(
|
|
159
|
+
backup_dir.parent / "updated_pdfs.txt", sorted(merged_pdfs), "updated_pdfs"
|
|
160
|
+
)
|
|
156
161
|
except Exception as exc: # noqa: BLE001
|
|
157
162
|
logging.error("Conversion error: %s", exc)
|
|
158
|
-
print_error(f"
|
|
163
|
+
print_error(f" ERR - PDF conversion failed: {exc}")
|
|
159
164
|
return 1
|
|
160
165
|
else:
|
|
161
166
|
print("\n[2/3] PDF conversion skipped (--skip-convert)")
|
|
@@ -185,13 +190,13 @@ def run_pipeline(
|
|
|
185
190
|
print(f" AI OCR provider: {ai_provider} ({provider.model})")
|
|
186
191
|
else:
|
|
187
192
|
print_warn(
|
|
188
|
-
f"
|
|
193
|
+
f" WRN - AI provider '{ai_provider}' not available "
|
|
189
194
|
"(missing API key or package). OCR skipped."
|
|
190
195
|
)
|
|
191
196
|
except (ValueError, ImportError) as exc:
|
|
192
197
|
logging.warning("Could not initialise AI provider: %s", exc)
|
|
193
198
|
elif use_ai_ocr:
|
|
194
|
-
print_warn("
|
|
199
|
+
print_warn(" WRN - --use-ai-ocr set but no --ai-provider given. OCR skipped.")
|
|
195
200
|
|
|
196
201
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else ["remarkable"]
|
|
197
202
|
|
|
@@ -241,15 +246,17 @@ def run_pipeline(
|
|
|
241
246
|
|
|
242
247
|
if not notebooks:
|
|
243
248
|
print(" No notebooks to export — skipping")
|
|
244
|
-
exported, skipped = 0, 0
|
|
249
|
+
exported, skipped, exported_dirs = 0, 0, []
|
|
245
250
|
else:
|
|
246
|
-
exported, skipped = exporter.export_all(
|
|
251
|
+
exported, skipped, exported_dirs = exporter.export_all(
|
|
247
252
|
notebooks=notebooks,
|
|
248
253
|
pdf_output_dir=pdf_output_dir,
|
|
249
254
|
force=force_export,
|
|
250
255
|
converted_pages=converted_pages,
|
|
251
256
|
page_filter=page_filter,
|
|
257
|
+
updated_pages=updated_pages,
|
|
252
258
|
)
|
|
259
|
+
write_manifest(backup_dir.parent / "updated_md.txt", sorted(exported_dirs), "updated_md")
|
|
253
260
|
|
|
254
261
|
# ------------------------------------------------------------------
|
|
255
262
|
# Summary
|
|
@@ -41,7 +41,8 @@ def run_sync_command(
|
|
|
41
41
|
"""
|
|
42
42
|
import time as _time
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
log_dir = backup_dir.parent
|
|
45
|
+
setup_logging(log_level, log_dir=log_dir)
|
|
45
46
|
_start_time = _time.monotonic()
|
|
46
47
|
|
|
47
48
|
print("ReMarkable Sync (Backup + Convert)")
|
|
@@ -330,7 +330,7 @@ class _WatchTray:
|
|
|
330
330
|
|
|
331
331
|
def _on_open_log(self, icon, item):
|
|
332
332
|
if self._backup_dir:
|
|
333
|
-
log_file = self._backup_dir / "remarkablesync.log"
|
|
333
|
+
log_file = self._backup_dir.parent / "remarkablesync.log"
|
|
334
334
|
if log_file.exists():
|
|
335
335
|
self._open_file(log_file)
|
|
336
336
|
|
|
@@ -738,7 +738,7 @@ class _TrayLogHandler(logging.Handler):
|
|
|
738
738
|
return
|
|
739
739
|
self._tray.set_detail(msg)
|
|
740
740
|
|
|
741
|
-
# Parse progress from page callbacks: "PDF: Work (page 3/21)"
|
|
741
|
+
# Parse progress from page callbacks: "PDF: Work (page 3/21)" or "PDF: Work (page 3/21) [cached]"
|
|
742
742
|
import re
|
|
743
743
|
|
|
744
744
|
m = re.search(r"(PDF|MD): (.+?) \(page (\d+)/(\d+)\)", msg)
|
|
@@ -49,7 +49,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
49
49
|
"wifi_host": "",
|
|
50
50
|
"password": "",
|
|
51
51
|
"folders": [],
|
|
52
|
-
"sync_actions": ["pdf"],
|
|
52
|
+
"sync_actions": ["backup", "pdf", "ocr"],
|
|
53
53
|
"ocr_enabled": False,
|
|
54
54
|
"ocr_output_dir": "",
|
|
55
55
|
"output_dir": "",
|
|
@@ -83,6 +83,16 @@ def load_config() -> Dict[str, Any]:
|
|
|
83
83
|
# Merge with defaults so new keys are always present
|
|
84
84
|
merged = dict(DEFAULT_CONFIG)
|
|
85
85
|
merged.update(data)
|
|
86
|
+
|
|
87
|
+
# Cascade-normalize sync_actions: if a later step is present,
|
|
88
|
+
# all earlier steps must be too (e.g. "pdf" implies "backup").
|
|
89
|
+
_action_order = [a for a, _ in SYNC_ACTIONS]
|
|
90
|
+
actions = merged.get("sync_actions", [])
|
|
91
|
+
valid = [a for a in actions if a in _action_order]
|
|
92
|
+
if valid:
|
|
93
|
+
highest = max(_action_order.index(a) for a in valid)
|
|
94
|
+
merged["sync_actions"] = _action_order[: highest + 1]
|
|
95
|
+
|
|
86
96
|
return merged
|
|
87
97
|
except (json.JSONDecodeError, OSError):
|
|
88
98
|
return dict(DEFAULT_CONFIG)
|