supynote 0.8.0__py3-none-any.whl
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.
- supynote/__init__.py +0 -0
- supynote/application/__init__.py +0 -0
- supynote/application/dtos/browse_dto.py +40 -0
- supynote/application/dtos/device_dto.py +42 -0
- supynote/application/dtos/device_info_dto.py +50 -0
- supynote/application/dtos/download_dto.py +96 -0
- supynote/application/dtos/list_files_dto.py +50 -0
- supynote/application/use_cases/browse_device.py +69 -0
- supynote/application/use_cases/download_use_case.py +250 -0
- supynote/application/use_cases/find_device.py +101 -0
- supynote/application/use_cases/get_device_info.py +73 -0
- supynote/application/use_cases/list_files.py +91 -0
- supynote/cli.py +153 -0
- supynote/converter.py +364 -0
- supynote/device_finder.py +94 -0
- supynote/domain/__init__.py +0 -0
- supynote/domain/device_management/__init__.py +0 -0
- supynote/domain/device_management/entities/device.py +152 -0
- supynote/domain/device_management/repositories/device_repository.py +51 -0
- supynote/domain/device_management/value_objects/device_connection.py +134 -0
- supynote/domain/note_management/__init__.py +0 -0
- supynote/domain/note_management/entities/note.py +201 -0
- supynote/domain/note_management/repositories/note_repository.py +72 -0
- supynote/domain/note_management/services/conversion_service.py +97 -0
- supynote/domain/note_management/value_objects/note_id.py +41 -0
- supynote/domain/note_management/value_objects/note_path.py +93 -0
- supynote/domain/note_management/value_objects/time_range_filter.py +81 -0
- supynote/domain/shared/__init__.py +0 -0
- supynote/domain/shared/base_entity.py +62 -0
- supynote/domain/shared/base_value_object.py +64 -0
- supynote/infrastructure/__init__.py +0 -0
- supynote/infrastructure/network/network_discovery_service.py +60 -0
- supynote/infrastructure/repositories/memory_device_repository.py +39 -0
- supynote/infrastructure/repositories/supernote_remote_repository.py +203 -0
- supynote/merger.py +517 -0
- supynote/ocr/__init__.py +6 -0
- supynote/ocr/entities.py +51 -0
- supynote/ocr/llava_service.py +212 -0
- supynote/ocr/native_service.py +581 -0
- supynote/ocr/pdf_processor.py +128 -0
- supynote/ocr/services.py +191 -0
- supynote/ocr/trocr_service.py +290 -0
- supynote/pdf_merger.py +196 -0
- supynote/presentation/__init__.py +0 -0
- supynote/presentation/cli/__init__.py +0 -0
- supynote/presentation/cli/commands/browse_command.py +33 -0
- supynote/presentation/cli/commands/commands.py +64 -0
- supynote/presentation/cli/commands/find_command.py +50 -0
- supynote/presentation/cli/commands/info_command.py +42 -0
- supynote/presentation/cli/commands/list_command.py +41 -0
- supynote/presentation/cli/container.py +114 -0
- supynote/presentation/cli/dispatcher.py +68 -0
- supynote/presentation/cli/main_refactored.py +74 -0
- supynote/services/post_processing_service.py +115 -0
- supynote/supernote.py +507 -0
- supynote-0.8.0.dist-info/METADATA +248 -0
- supynote-0.8.0.dist-info/RECORD +60 -0
- supynote-0.8.0.dist-info/WHEEL +4 -0
- supynote-0.8.0.dist-info/entry_points.txt +2 -0
- supynote-0.8.0.dist-info/licenses/LICENSE +21 -0
supynote/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""DTOs for the Browse Device use case."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class BrowseDeviceRequest:
|
|
8
|
+
"""Request DTO for browsing a device."""
|
|
9
|
+
|
|
10
|
+
ip: Optional[str] = None
|
|
11
|
+
port: str = "8089"
|
|
12
|
+
open_in_browser: bool = True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class BrowseDeviceResponse:
|
|
17
|
+
"""Response DTO for browsing a device."""
|
|
18
|
+
|
|
19
|
+
success: bool
|
|
20
|
+
url: Optional[str] = None
|
|
21
|
+
device_ip: Optional[str] = None
|
|
22
|
+
message: str = ""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def success_opened(cls, url: str, device_ip: str) -> "BrowseDeviceResponse":
|
|
26
|
+
"""Create a success response when browser was opened."""
|
|
27
|
+
return cls(
|
|
28
|
+
success=True,
|
|
29
|
+
url=url,
|
|
30
|
+
device_ip=device_ip,
|
|
31
|
+
message=f"š Opening {url} in browser..."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def device_not_found(cls) -> "BrowseDeviceResponse":
|
|
36
|
+
"""Create a response when no device was found."""
|
|
37
|
+
return cls(
|
|
38
|
+
success=False,
|
|
39
|
+
message="ā No Supernote device found. Use --ip to specify manually."
|
|
40
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Data Transfer Objects for device operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class FindDeviceRequest:
|
|
9
|
+
"""Request DTO for finding a device."""
|
|
10
|
+
ip: Optional[str] = None # Explicitly provided IP
|
|
11
|
+
network_range: Optional[str] = None
|
|
12
|
+
open_in_browser: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FindDeviceResponse:
|
|
17
|
+
"""Response DTO for finding a device."""
|
|
18
|
+
found: bool
|
|
19
|
+
ip: Optional[str] = None # Simplified name
|
|
20
|
+
ip_address: Optional[str] = None # Keep for compatibility
|
|
21
|
+
port: Optional[str] = None
|
|
22
|
+
url: Optional[str] = None
|
|
23
|
+
device_name: Optional[str] = None
|
|
24
|
+
source: Optional[str] = None # "provided", "network", "stored"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def not_found(cls) -> 'FindDeviceResponse':
|
|
28
|
+
"""Create a response for when no device is found."""
|
|
29
|
+
return cls(found=False)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def success(cls, ip: str, port: str, url: str, name: str, source: str = "network") -> 'FindDeviceResponse':
|
|
33
|
+
"""Create a successful response."""
|
|
34
|
+
return cls(
|
|
35
|
+
found=True,
|
|
36
|
+
ip=ip,
|
|
37
|
+
ip_address=ip, # Duplicate for compatibility
|
|
38
|
+
port=port,
|
|
39
|
+
url=url,
|
|
40
|
+
device_name=name,
|
|
41
|
+
source=source
|
|
42
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""DTOs for the Device Info use case."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class DeviceInfoRequest:
|
|
8
|
+
"""Request DTO for getting device information."""
|
|
9
|
+
|
|
10
|
+
ip: Optional[str] = None
|
|
11
|
+
port: str = "8089"
|
|
12
|
+
output_directory: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DeviceInfoResponse:
|
|
17
|
+
"""Response DTO for device information."""
|
|
18
|
+
|
|
19
|
+
success: bool
|
|
20
|
+
ip: Optional[str] = None
|
|
21
|
+
port: Optional[str] = None
|
|
22
|
+
url: Optional[str] = None
|
|
23
|
+
status: Optional[str] = None
|
|
24
|
+
output_directory: Optional[str] = None
|
|
25
|
+
message: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def success_with_info(
|
|
29
|
+
cls,
|
|
30
|
+
ip: str,
|
|
31
|
+
port: str,
|
|
32
|
+
output_directory: Optional[str] = None
|
|
33
|
+
) -> "DeviceInfoResponse":
|
|
34
|
+
"""Create a success response with device info."""
|
|
35
|
+
return cls(
|
|
36
|
+
success=True,
|
|
37
|
+
ip=ip,
|
|
38
|
+
port=port,
|
|
39
|
+
url=f"http://{ip}:{port}",
|
|
40
|
+
status="š¢ Connected",
|
|
41
|
+
output_directory=output_directory
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def device_not_found(cls) -> "DeviceInfoResponse":
|
|
46
|
+
"""Create a response when no device was found."""
|
|
47
|
+
return cls(
|
|
48
|
+
success=False,
|
|
49
|
+
message="ā No Supernote device found. Use --ip to specify manually."
|
|
50
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Data Transfer Objects for download operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from ...domain.note_management.value_objects.time_range_filter import TimeRange
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DownloadMode(Enum):
|
|
11
|
+
"""Download execution mode."""
|
|
12
|
+
SYNC = "sync"
|
|
13
|
+
ASYNC = "async"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowStep(Enum):
|
|
17
|
+
"""OCR workflow steps."""
|
|
18
|
+
DOWNLOAD = "download"
|
|
19
|
+
CONVERT = "convert"
|
|
20
|
+
OCR = "ocr"
|
|
21
|
+
MERGE = "merge"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DownloadNotesRequest:
|
|
26
|
+
"""Request DTO for downloading notes."""
|
|
27
|
+
path: str
|
|
28
|
+
output_directory: Optional[str] = None
|
|
29
|
+
force: bool = False
|
|
30
|
+
check_size: bool = True
|
|
31
|
+
time_range: TimeRange = TimeRange.ALL
|
|
32
|
+
max_workers: int = 20
|
|
33
|
+
download_mode: DownloadMode = DownloadMode.ASYNC
|
|
34
|
+
|
|
35
|
+
# Workflow options
|
|
36
|
+
convert_pdf: bool = False
|
|
37
|
+
enable_ocr: bool = False
|
|
38
|
+
merge_by_date: bool = False
|
|
39
|
+
conversion_workers: int = 8
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class DownloadProgress:
|
|
44
|
+
"""Progress information for download operations."""
|
|
45
|
+
current: int
|
|
46
|
+
total: int
|
|
47
|
+
current_file: Optional[str] = None
|
|
48
|
+
phase: WorkflowStep = WorkflowStep.DOWNLOAD
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def percentage(self) -> float:
|
|
52
|
+
"""Get completion percentage."""
|
|
53
|
+
if self.total == 0:
|
|
54
|
+
return 0.0
|
|
55
|
+
return (self.current / self.total) * 100.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class WorkflowSummary:
|
|
60
|
+
"""Summary of completed workflow steps."""
|
|
61
|
+
downloaded_files: int = 0
|
|
62
|
+
converted_files: int = 0
|
|
63
|
+
ocr_processed_files: int = 0
|
|
64
|
+
merged_pdfs: int = 0
|
|
65
|
+
failed_downloads: int = 0
|
|
66
|
+
failed_conversions: int = 0
|
|
67
|
+
failed_ocr: int = 0
|
|
68
|
+
skipped_files: int = 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class DownloadNotesResponse:
|
|
73
|
+
"""Response DTO for downloading notes."""
|
|
74
|
+
success: bool
|
|
75
|
+
total_files: int
|
|
76
|
+
workflow_summary: WorkflowSummary
|
|
77
|
+
error_message: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def success_response(cls, total_files: int, summary: WorkflowSummary) -> 'DownloadNotesResponse':
|
|
81
|
+
"""Create a successful response."""
|
|
82
|
+
return cls(
|
|
83
|
+
success=True,
|
|
84
|
+
total_files=total_files,
|
|
85
|
+
workflow_summary=summary
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def failure_response(cls, error_message: str) -> 'DownloadNotesResponse':
|
|
90
|
+
"""Create a failure response."""
|
|
91
|
+
return cls(
|
|
92
|
+
success=False,
|
|
93
|
+
total_files=0,
|
|
94
|
+
workflow_summary=WorkflowSummary(),
|
|
95
|
+
error_message=error_message
|
|
96
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""DTOs for the List Files use case."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FileItem:
|
|
8
|
+
"""Represents a file or directory item."""
|
|
9
|
+
name: str
|
|
10
|
+
is_directory: bool
|
|
11
|
+
date: Optional[str] = None
|
|
12
|
+
size: Optional[int] = None
|
|
13
|
+
path: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ListFilesRequest:
|
|
18
|
+
"""Request DTO for listing files."""
|
|
19
|
+
|
|
20
|
+
ip: Optional[str] = None
|
|
21
|
+
port: str = "8089"
|
|
22
|
+
directory: str = "" # Empty string means root
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ListFilesResponse:
|
|
27
|
+
"""Response DTO for listing files."""
|
|
28
|
+
|
|
29
|
+
success: bool
|
|
30
|
+
directory: str = ""
|
|
31
|
+
files: List[FileItem] = None
|
|
32
|
+
message: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def success_with_files(cls, directory: str, files: List[FileItem]) -> "ListFilesResponse":
|
|
36
|
+
"""Create a success response with file list."""
|
|
37
|
+
return cls(
|
|
38
|
+
success=True,
|
|
39
|
+
directory=directory or "root",
|
|
40
|
+
files=files or []
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def error(cls, message: str) -> "ListFilesResponse":
|
|
45
|
+
"""Create an error response."""
|
|
46
|
+
return cls(
|
|
47
|
+
success=False,
|
|
48
|
+
files=[],
|
|
49
|
+
message=message
|
|
50
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Use case for browsing a device in web browser."""
|
|
2
|
+
import webbrowser
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ..dtos.browse_dto import BrowseDeviceRequest, BrowseDeviceResponse
|
|
6
|
+
from ...domain.device_management.repositories.device_repository import DeviceRepository
|
|
7
|
+
from ...domain.device_management.value_objects.device_connection import DeviceConnection
|
|
8
|
+
from ...infrastructure.network.network_discovery_service import NetworkDiscoveryService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrowseDeviceUseCase:
|
|
12
|
+
"""Use case for opening device web interface in browser."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
device_repository: DeviceRepository,
|
|
17
|
+
discovery_service: NetworkDiscoveryService
|
|
18
|
+
):
|
|
19
|
+
"""Initialize the use case."""
|
|
20
|
+
self._device_repository = device_repository
|
|
21
|
+
self._discovery_service = discovery_service
|
|
22
|
+
|
|
23
|
+
def execute(self, request: BrowseDeviceRequest) -> BrowseDeviceResponse:
|
|
24
|
+
"""
|
|
25
|
+
Execute the browse device use case.
|
|
26
|
+
|
|
27
|
+
1. Find or discover device
|
|
28
|
+
2. Build URL
|
|
29
|
+
3. Open in browser
|
|
30
|
+
"""
|
|
31
|
+
# Try to find device
|
|
32
|
+
device_ip = self._find_or_discover_device(request.ip)
|
|
33
|
+
|
|
34
|
+
if not device_ip:
|
|
35
|
+
return BrowseDeviceResponse.device_not_found()
|
|
36
|
+
|
|
37
|
+
# Build URL
|
|
38
|
+
url = f"http://{device_ip}:{request.port}"
|
|
39
|
+
|
|
40
|
+
# Open in browser if requested
|
|
41
|
+
if request.open_in_browser:
|
|
42
|
+
webbrowser.open(url)
|
|
43
|
+
|
|
44
|
+
return BrowseDeviceResponse.success_opened(url, device_ip)
|
|
45
|
+
|
|
46
|
+
def _find_or_discover_device(self, requested_ip: Optional[str]) -> Optional[str]:
|
|
47
|
+
"""Find device from repository or discover on network."""
|
|
48
|
+
# If IP was provided, use it directly
|
|
49
|
+
if requested_ip:
|
|
50
|
+
return requested_ip
|
|
51
|
+
|
|
52
|
+
# Try to find in repository first
|
|
53
|
+
connection = DeviceConnection.for_discovery()
|
|
54
|
+
stored_device = self._device_repository.find_by_connection(connection)
|
|
55
|
+
|
|
56
|
+
if stored_device:
|
|
57
|
+
return stored_device.connection.ip_address.value
|
|
58
|
+
|
|
59
|
+
# Discover on network
|
|
60
|
+
discovered_ip = self._discovery_service.discover_device()
|
|
61
|
+
|
|
62
|
+
if discovered_ip:
|
|
63
|
+
# Store for future use
|
|
64
|
+
from ...domain.device_management.entities.device import Device
|
|
65
|
+
device = Device.discover(discovered_ip, str(connection.port.value))
|
|
66
|
+
self._device_repository.save(device)
|
|
67
|
+
return discovered_ip
|
|
68
|
+
|
|
69
|
+
return None
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Use case for download and related operations."""
|
|
2
|
+
from typing import Optional, Any, Dict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from ...infrastructure.network.network_discovery_service import NetworkDiscoveryService
|
|
7
|
+
from ...domain.device_management.repositories.device_repository import DeviceRepository
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DownloadUseCase:
|
|
11
|
+
"""Handles download and related operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
device_repository: DeviceRepository,
|
|
16
|
+
discovery_service: NetworkDiscoveryService
|
|
17
|
+
):
|
|
18
|
+
"""Initialize the adapter."""
|
|
19
|
+
self._device_repository = device_repository
|
|
20
|
+
self._discovery_service = discovery_service
|
|
21
|
+
|
|
22
|
+
def execute_download(self, args: Any) -> bool:
|
|
23
|
+
"""Execute download command using legacy code."""
|
|
24
|
+
ip = self._get_device_ip(args)
|
|
25
|
+
if not ip:
|
|
26
|
+
print("ā No Supernote device found. Use --ip to specify manually.")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
from ...supernote import Supernote
|
|
30
|
+
from ...converter import PDFConverter
|
|
31
|
+
|
|
32
|
+
device = Supernote(ip, args.port, args.output)
|
|
33
|
+
|
|
34
|
+
if args.use_async:
|
|
35
|
+
# Use high-performance async downloader
|
|
36
|
+
async def async_download():
|
|
37
|
+
try:
|
|
38
|
+
if "/" in args.path and not args.path.endswith("/"):
|
|
39
|
+
# Downloading a specific file
|
|
40
|
+
success = device.download_file(args.path, force=args.force, check_size=args.check_size)
|
|
41
|
+
if success and args.convert_pdf and args.path.lower().endswith('.note'):
|
|
42
|
+
local_file = device.local_root / args.path.lstrip('/')
|
|
43
|
+
converter = PDFConverter(vectorize=True, enable_links=True)
|
|
44
|
+
converter.convert_file(local_file)
|
|
45
|
+
else:
|
|
46
|
+
# Downloading a directory with async
|
|
47
|
+
success, total = await device.download_directory_async(
|
|
48
|
+
args.path, args.workers, args.force, args.check_size,
|
|
49
|
+
time_range=args.time_range)
|
|
50
|
+
print(f"š Async download completed: {success}/{total} files")
|
|
51
|
+
|
|
52
|
+
if args.convert_pdf:
|
|
53
|
+
# Convert downloaded files
|
|
54
|
+
converter = PDFConverter(vectorize=True, enable_links=True, verbose=getattr(args, 'verbose', False))
|
|
55
|
+
local_dir = device.raw_dir / args.path.lstrip('/')
|
|
56
|
+
if local_dir.exists():
|
|
57
|
+
converter.convert_directory(local_dir, max_workers=args.conversion_workers, time_range=args.time_range)
|
|
58
|
+
|
|
59
|
+
# Handle OCR and merger processing
|
|
60
|
+
from ...services.post_processing_service import PostProcessingService
|
|
61
|
+
post_processor = PostProcessingService()
|
|
62
|
+
|
|
63
|
+
# Use processed_output if provided
|
|
64
|
+
output_dir = Path(args.processed_output) if getattr(args, 'processed_output', None) else None
|
|
65
|
+
post_processor.process_downloaded_files(local_dir, device, args, args.conversion_workers, output_dir)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"ā Download failed: {e}")
|
|
68
|
+
return False
|
|
69
|
+
finally:
|
|
70
|
+
# Clean up async session
|
|
71
|
+
await device.close_async()
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
return asyncio.run(async_download())
|
|
75
|
+
else:
|
|
76
|
+
# Sync download
|
|
77
|
+
if "/" in args.path and not args.path.endswith("/"):
|
|
78
|
+
return device.download_file(args.path, force=args.force)
|
|
79
|
+
else:
|
|
80
|
+
success, total = device.download_directory(args.path, args.workers, args.force)
|
|
81
|
+
print(f"Downloaded {success}/{total} files")
|
|
82
|
+
return success > 0
|
|
83
|
+
|
|
84
|
+
def execute_convert(self, args: Any) -> bool:
|
|
85
|
+
"""Execute convert command using legacy code."""
|
|
86
|
+
from ...converter import PDFConverter
|
|
87
|
+
|
|
88
|
+
input_path = Path(args.path)
|
|
89
|
+
if not input_path.exists():
|
|
90
|
+
print(f"ā Path does not exist: {input_path}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
converter = PDFConverter(
|
|
94
|
+
vectorize=not args.no_vector,
|
|
95
|
+
enable_links=not args.no_links
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if input_path.is_file():
|
|
99
|
+
output_dir = Path(args.output) if args.output else None
|
|
100
|
+
converter.convert_file(input_path, output_dir)
|
|
101
|
+
elif input_path.is_dir():
|
|
102
|
+
output_dir = Path(args.output) if args.output else None
|
|
103
|
+
converter.convert_directory(input_path, output_dir, recursive=args.recursive, max_workers=args.workers)
|
|
104
|
+
else:
|
|
105
|
+
print(f"ā Invalid path: {input_path}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def execute_validate(self, args: Any) -> bool:
|
|
111
|
+
"""Execute validate command using legacy code."""
|
|
112
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
113
|
+
from ...converter import PDFConverter
|
|
114
|
+
|
|
115
|
+
directory = Path(args.directory)
|
|
116
|
+
if not directory.exists():
|
|
117
|
+
print(f"ā Directory does not exist: {directory}")
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# Find all .note files
|
|
121
|
+
note_files = list(directory.rglob("*.note"))
|
|
122
|
+
print(f"š Found {len(note_files)} .note files to validate")
|
|
123
|
+
|
|
124
|
+
converter = PDFConverter()
|
|
125
|
+
problematic_files = []
|
|
126
|
+
|
|
127
|
+
def validate_file(file_path):
|
|
128
|
+
try:
|
|
129
|
+
# Try to convert to validate
|
|
130
|
+
converter.convert_file(file_path, Path("/tmp"))
|
|
131
|
+
return file_path, True, None
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return file_path, False, str(e)
|
|
134
|
+
|
|
135
|
+
with ThreadPoolExecutor(max_workers=args.workers) as executor:
|
|
136
|
+
futures = {executor.submit(validate_file, f): f for f in note_files}
|
|
137
|
+
|
|
138
|
+
for future in as_completed(futures):
|
|
139
|
+
file_path, success, error = future.result()
|
|
140
|
+
if not success:
|
|
141
|
+
problematic_files.append((file_path, error))
|
|
142
|
+
print(f"ā {file_path}: {error}")
|
|
143
|
+
|
|
144
|
+
if problematic_files:
|
|
145
|
+
print(f"\nā ļø Found {len(problematic_files)} problematic files")
|
|
146
|
+
|
|
147
|
+
if args.fix:
|
|
148
|
+
ip = self._get_device_ip(args)
|
|
149
|
+
if ip:
|
|
150
|
+
print("š§ Re-downloading problematic files...")
|
|
151
|
+
from ...supernote import Supernote
|
|
152
|
+
device = Supernote(ip, args.port)
|
|
153
|
+
|
|
154
|
+
for file_path, _ in problematic_files:
|
|
155
|
+
relative_path = file_path.relative_to(directory)
|
|
156
|
+
device.download_file(str(relative_path), force=True)
|
|
157
|
+
|
|
158
|
+
if args.convert:
|
|
159
|
+
converter.convert_file(file_path)
|
|
160
|
+
else:
|
|
161
|
+
print("ā
All files validated successfully")
|
|
162
|
+
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def execute_ocr(self, args: Any) -> bool:
|
|
166
|
+
"""Execute OCR command using legacy code."""
|
|
167
|
+
input_path = Path(args.input)
|
|
168
|
+
|
|
169
|
+
if not input_path.exists():
|
|
170
|
+
print(f"ā Path does not exist: {input_path}")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
if args.engine == "native":
|
|
174
|
+
from ...ocr.native_service import NativeSupernoteService
|
|
175
|
+
service = NativeSupernoteService()
|
|
176
|
+
|
|
177
|
+
if args.batch and input_path.is_dir():
|
|
178
|
+
# Batch process directory
|
|
179
|
+
note_files = list(input_path.glob("*.note"))
|
|
180
|
+
print(f"š Found {len(note_files)} .note files to process")
|
|
181
|
+
|
|
182
|
+
for note_file in note_files:
|
|
183
|
+
output_file = note_file.with_suffix('.pdf')
|
|
184
|
+
if args.output:
|
|
185
|
+
output_dir = Path(args.output)
|
|
186
|
+
output_dir.mkdir(exist_ok=True)
|
|
187
|
+
output_file = output_dir / output_file.name
|
|
188
|
+
|
|
189
|
+
print(f"š Processing {note_file.name}...")
|
|
190
|
+
service.convert_note_to_searchable_pdf(note_file, output_file)
|
|
191
|
+
else:
|
|
192
|
+
# Single file
|
|
193
|
+
output_file = Path(args.output) if args.output else input_path.with_suffix('.pdf')
|
|
194
|
+
service.convert_note_to_searchable_pdf(input_path, output_file)
|
|
195
|
+
else:
|
|
196
|
+
print(f"ā OCR engine '{args.engine}' not yet implemented in DDD")
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def execute_merge(self, args: Any) -> bool:
|
|
202
|
+
"""Execute merge command using DateBasedMerger."""
|
|
203
|
+
from ...merger import DateBasedMerger, MergeConfig
|
|
204
|
+
import os
|
|
205
|
+
|
|
206
|
+
directory = Path(args.directory)
|
|
207
|
+
if not directory.exists():
|
|
208
|
+
print(f"ā Directory does not exist: {directory}")
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
# Get journals directory from env var or args
|
|
212
|
+
journals_dir_str = os.environ.get("SUPYNOTE_JOURNALS_DIR") or getattr(args, 'journals_dir', None)
|
|
213
|
+
journals_dir = Path(journals_dir_str) if journals_dir_str else None
|
|
214
|
+
|
|
215
|
+
# Get assets directory from env var if set
|
|
216
|
+
assets_dir_str = os.environ.get("SUPYNOTE_ASSETS_DIR")
|
|
217
|
+
assets_dir = Path(assets_dir_str) if assets_dir_str else None
|
|
218
|
+
|
|
219
|
+
merge_config = MergeConfig(
|
|
220
|
+
pdf_output_dir=args.pdf_output,
|
|
221
|
+
markdown_output_dir=args.markdown_output,
|
|
222
|
+
time_range=args.time_range,
|
|
223
|
+
journals_dir=journals_dir,
|
|
224
|
+
assets_dir=assets_dir
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
merger = DateBasedMerger(merge_config)
|
|
228
|
+
|
|
229
|
+
if args.pdf_only:
|
|
230
|
+
merger.merge_pdfs_by_date(directory)
|
|
231
|
+
elif args.markdown_only:
|
|
232
|
+
merger.merge_markdown_by_date(directory)
|
|
233
|
+
else:
|
|
234
|
+
merger.merge_all_by_date(directory)
|
|
235
|
+
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
def _get_device_ip(self, args: Any) -> Optional[str]:
|
|
239
|
+
"""Get device IP from args or discovery."""
|
|
240
|
+
if hasattr(args, 'ip') and args.ip:
|
|
241
|
+
return args.ip
|
|
242
|
+
|
|
243
|
+
# Try discovery
|
|
244
|
+
ip = self._discovery_service.discover_device()
|
|
245
|
+
if ip:
|
|
246
|
+
from ...domain.device_management.entities.device import Device
|
|
247
|
+
device = Device.discover(ip, "8089")
|
|
248
|
+
self._device_repository.save(device)
|
|
249
|
+
|
|
250
|
+
return ip
|