class RemarkableCloudManager
Unified manager for reMarkable Cloud operations that uses REST API as primary method with rmcl library as fallback, handling authentication, file operations, and folder management.
/tf/active/vicechatdev/e-ink-llm/remarkable_cloud.py
69 - 458
complex
Purpose
This class provides a comprehensive interface for interacting with reMarkable Cloud storage. It abstracts away the complexity of using multiple client libraries (REST API and rmcl) by automatically selecting the best available method and falling back when needed. The class handles authentication, document upload/download, folder creation and navigation, and maintains caching for improved performance. It's designed to be resilient, attempting multiple authentication methods and providing detailed logging and user feedback throughout operations.
Source Code
class RemarkableCloudManager:
"""
Unified reMarkable Cloud manager that uses REST API as primary method
with rmcl library as fallback for compatibility
"""
def __init__(self, config_dir: Optional[str] = None, prefer_rest_api: bool = True):
self.config_dir = Path(config_dir) if config_dir else Path.home() / '.eink-llm'
self.config_dir.mkdir(exist_ok=True)
self.device_token_file = self.config_dir / 'remarkable_device_token'
self.config_file = self.config_dir / 'remarkable_config.json'
self.logger = logging.getLogger(__name__)
self.authenticated = False
self.last_sync_time = None
self.prefer_rest_api = prefer_rest_api
# Initialize the appropriate client
self.rest_client = RemarkableRestClient(config_dir)
self.rmcl_client = None
# Try to initialize rmcl client if available and requested
if RMCL_AVAILABLE and not prefer_rest_api:
try:
# rmcl initialization would go here
self.client_type = "rmcl"
print("🔧 Using rmcl library")
except Exception as e:
self.logger.warning(f"rmcl initialization failed: {e}")
self.client_type = "rest"
print("🔧 Falling back to REST API")
else:
self.client_type = "rest"
print("🔧 Using REST API client")
# Cache for folder structure and file metadata (REST API only)
self.folder_cache: Dict[str, Dict] = {}
self.file_cache: Dict[str, Dict] = {}
self.cache_timestamp = None
async def authenticate(self, one_time_code: Optional[str] = None) -> bool:
"""
Authenticate with reMarkable Cloud using the selected client
Args:
one_time_code: One-time code from reMarkable account (required for first setup)
Returns:
True if authentication successful, False otherwise
"""
try:
# First try REST API (primary method)
if self.client_type == "rest" or self.prefer_rest_api:
print("🔧 Attempting authentication with REST API...")
success = await self.rest_client.authenticate(one_time_code)
if success:
self.authenticated = success
self.client_type = "rest"
return success
else:
print("⚠️ REST API authentication failed")
# If REST API failed and rmcl is available, try fallback
if RMCL_AVAILABLE:
print("🔄 Falling back to rmcl library...")
self.client_type = "rmcl"
else:
print("❌ No fallback available - rmcl library not installed")
print("💡 To install rmcl fallback: pip install rmcl")
return False
# Try rmcl client (either as primary choice or fallback)
if self.client_type == "rmcl" and RMCL_AVAILABLE:
print("🔧 Using rmcl library for authentication...")
# Check if we have a device token already
if self.device_token_file.exists() and not one_time_code:
print("🔑 Using existing device token with rmcl...")
try:
# Test existing authentication
root = Item.get_by_id_s('') # Get root folder
self.authenticated = True
print("✅ rmcl authentication successful!")
return True
except (AuthError, ApiError) as e:
print("⚠️ Existing token invalid, need fresh one-time code")
# First time setup or token refresh - need one-time code
if one_time_code:
print("🔐 Registering new device with rmcl...")
try:
Item.register_device(one_time_code)
# Test the registration worked
root = Item.get_by_id_s('') # Get root folder
print("✅ rmcl device registered and authenticated!")
self.authenticated = True
return True
except Exception as e:
print(f"❌ rmcl registration failed: {e}")
return False
print("❌ rmcl requires a one-time code for authentication")
return False
print("❌ No available authentication method")
print("📝 Options:")
print(" 1. Generate a one-time code from https://my.remarkable.com/connect/desktop")
print(" 2. Install rmcl library for fallback: pip install rmcl")
return False
except Exception as e:
self.logger.error(f"Authentication error: {e}")
print(f"❌ Authentication error: {e}")
# If we haven't tried rmcl yet and it's available, try it as last resort
if self.client_type == "rest" and RMCL_AVAILABLE and one_time_code:
print("🔄 Trying rmcl as last resort...")
self.client_type = "rmcl"
return await self.authenticate(one_time_code)
return False
async def get_folder_by_path(self, folder_path: str) -> Optional[Union[Dict, Folder]]:
"""
Get a folder by its path (e.g., '/My Folder/Subfolder')
Args:
folder_path: Path to the folder, starting with '/' for root
Returns:
Folder object/dict if found, None otherwise
"""
try:
if self.client_type == "rest":
# For REST API, we'll work with folder names rather than paths
# This is a simplified implementation
if folder_path.strip() in ['/', '']:
return {"ID": "", "Type": "CollectionType", "VissibleName": "Root"}
# Find folder by name (simplified - could be enhanced for full path support)
folder_name = folder_path.strip('/').split('/')[-1]
folder_id = self.rest_client.find_folder_by_name(folder_name)
if folder_id:
return {"ID": folder_id, "Type": "CollectionType", "VissibleName": folder_name}
return None
elif self.client_type == "rmcl" and RMCL_AVAILABLE:
# Original rmcl implementation
current_folder = Item.get_by_id_s('') # Root folder has empty ID
if folder_path.strip() in ['/', '']:
return current_folder
# Split path and navigate through folders
path_parts = [part for part in folder_path.strip('/').split('/') if part]
for part in path_parts:
found = False
for child in current_folder.children:
if isinstance(child, Folder) and child.name == part:
current_folder = child
found = True
break
if not found:
print(f"❌ Folder not found: {part} in path {folder_path}")
return None
return current_folder
return None
except Exception as e:
self.logger.error(f"Error finding folder {folder_path}: {e}")
return None
async def list_files_in_folder(self, folder_path: str, include_subfolders: bool = True) -> List[Union[Dict, Document]]:
"""
List all PDF files in a folder and optionally its subfolders
Args:
folder_path: Path to the folder to scan
include_subfolders: If True, scan subfolders recursively
Returns:
List of Document objects/dicts representing PDF files
"""
try:
if self.client_type == "rest":
# For REST API implementation
if folder_path.strip() in ['/', '']:
folder_id = ""
else:
folder_name = folder_path.strip('/').split('/')[-1]
folder_id = self.rest_client.find_folder_by_name(folder_name)
if not folder_id:
print(f"❌ Folder not found: {folder_path}")
return []
documents = self.rest_client.get_documents_in_folder(folder_id)
# If include_subfolders is True, we'd need to implement recursive search
# This is simplified for now
return documents
elif self.client_type == "rmcl" and RMCL_AVAILABLE:
# Original rmcl implementation
folder = await self.get_folder_by_path(folder_path)
if not folder:
return []
documents = []
def collect_documents(current_folder: Folder):
for child in current_folder.children:
if isinstance(child, Document):
# Check if it's a PDF or has content we can process
documents.append(child)
elif isinstance(child, Folder) and include_subfolders:
collect_documents(child)
collect_documents(folder)
return documents
return []
except Exception as e:
self.logger.error(f"Error listing files in {folder_path}: {e}")
return []
async def download_document(self, document: Union[Dict, Document], output_dir: Path) -> Optional[Path]:
"""
Download a document from reMarkable Cloud
Args:
document: Document object/dict to download
output_dir: Directory to save the downloaded file
Returns:
Path to downloaded file, or None if failed
"""
try:
if self.client_type == "rest":
# REST API implementation
doc_id = document.get("ID") if isinstance(document, dict) else getattr(document, 'id', None)
doc_name = document.get("VissibleName") if isinstance(document, dict) else getattr(document, 'name', 'Unknown')
if not doc_id:
print("❌ No document ID available")
return None
return self.rest_client.download_document(doc_id, doc_name, output_dir)
elif self.client_type == "rmcl" and RMCL_AVAILABLE:
# Original rmcl implementation
output_dir.mkdir(parents=True, exist_ok=True)
# Generate safe filename
safe_name = "".join(c for c in document.name if c.isalnum() or c in (' ', '-', '_')).rstrip()
if not safe_name:
safe_name = f"document_{document.id[:8]}"
output_path = output_dir / f"{safe_name}.pdf"
# Download the document content
# First try to get the original PDF if it exists
try:
content = document.contents_s() # Get original PDF/EPUB
with open(output_path, 'wb') as f:
f.write(content.read())
print(f"📥 Downloaded: {document.name} -> {output_path.name}")
return output_path
except Exception:
# If no original content, try to get the raw file
raw_content = document.raw_s()
# Save as a temporary zip and try to extract PDF
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_file:
temp_file.write(raw_content.read())
temp_zip_path = Path(temp_file.name)
# For now, just save the raw content - in a real implementation,
# you might want to extract and convert notebook files to PDF
output_path = output_dir / f"{safe_name}.rm"
temp_zip_path.rename(output_path)
print(f"📥 Downloaded raw: {document.name} -> {output_path.name}")
return output_path
return None
except Exception as e:
doc_name = document.get("VissibleName", "Unknown") if isinstance(document, dict) else getattr(document, 'name', 'Unknown')
self.logger.error(f"Error downloading {doc_name}: {e}")
print(f"❌ Failed to download {doc_name}: {e}")
return None
async def upload_file(self, file_path: str, folder_path: str = None) -> Optional[str]:
"""Upload a file to reMarkable Cloud"""
try:
if self.client_type == 'rest':
return await self.rest_client.upload_file(file_path, folder_path)
else:
return await self.rmcl_client.upload_file(file_path, folder_path)
except Exception as e:
logger.error(f"Failed to upload file {file_path}: {e}")
return None
async def upload_content(self, content: bytes, filename: str, folder_path: str = None,
file_type: str = "application/pdf") -> Optional[str]:
"""Upload content directly to reMarkable Cloud"""
try:
if self.client_type == 'rest':
return await self.rest_client.upload_content(content, filename, folder_path, file_type)
else:
return await self.rmcl_client.upload_content(content, filename, folder_path, file_type)
except Exception as e:
logger.error(f"Failed to upload content {filename}: {e}")
return None
async def create_folder(self, folder_path: str) -> bool:
"""
Create a folder in reMarkable Cloud
Args:
folder_path: Full path to the folder to create
Returns:
True if folder created or already exists, False otherwise
"""
try:
# Check if folder already exists
existing = await self.get_folder_by_path(folder_path)
if existing:
print(f"📁 Folder already exists: {folder_path}")
return True
# Split path to get parent and folder name
path_parts = [part for part in folder_path.strip('/').split('/') if part]
if not path_parts:
print("❌ Invalid folder path")
return False
folder_name = path_parts[-1]
parent_path = '/' + '/'.join(path_parts[:-1]) if len(path_parts) > 1 else '/'
# Get parent folder
parent_folder = await self.get_folder_by_path(parent_path)
if not parent_folder:
print(f"❌ Parent folder not found: {parent_path}")
return False
# Create folder using REST API
success = await self.rest_client.create_folder(folder_name, parent_folder.id if parent_folder.id != 'root' else None)
if success:
print(f"✅ Folder created: {folder_path}")
# Invalidate cache
self.cache_timestamp = None
return True
else:
print(f"❌ Failed to create folder via REST API")
return False
except Exception as e:
self.logger.error(f"Error creating folder {folder_path}: {e}")
print(f"❌ Failed to create folder: {e}")
return False
def get_config(self) -> Dict:
"""Load configuration from file"""
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
return json.load(f)
except Exception as e:
self.logger.error(f"Error loading config: {e}")
return {}
return {}
def save_config(self, config: Dict) -> None:
"""Save configuration to file"""
try:
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=2)
except Exception as e:
self.logger.error(f"Error saving config: {e}")
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
- | - |
Parameter Details
config_dir: Optional path to configuration directory where authentication tokens and settings are stored. Defaults to '~/.eink-llm' if not provided. The directory will be created if it doesn't exist.
prefer_rest_api: Boolean flag indicating whether to prefer REST API over rmcl library. Defaults to True. When True, REST API is attempted first with rmcl as fallback. When False and rmcl is available, rmcl is used as primary method.
Return Value
Instantiation returns a RemarkableCloudManager object configured with the specified settings. Key method returns: authenticate() returns bool indicating success/failure; get_folder_by_path() returns Dict or Folder object or None; list_files_in_folder() returns List of Document objects/dicts; download_document() returns Path to downloaded file or None; upload_file() and upload_content() return Optional[str] document ID; create_folder() returns bool indicating success.
Class Interface
Methods
__init__(self, config_dir: Optional[str] = None, prefer_rest_api: bool = True)
Purpose: Initialize the RemarkableCloudManager with configuration directory and client preference
Parameters:
config_dir: Optional path to configuration directory, defaults to ~/.eink-llmprefer_rest_api: Whether to prefer REST API over rmcl library, defaults to True
Returns: None - initializes instance with configured clients and settings
async authenticate(self, one_time_code: Optional[str] = None) -> bool
Purpose: Authenticate with reMarkable Cloud using the selected client, with automatic fallback
Parameters:
one_time_code: One-time code from reMarkable account, required for first setup or token refresh
Returns: True if authentication successful, False otherwise
async get_folder_by_path(self, folder_path: str) -> Optional[Union[Dict, Folder]]
Purpose: Retrieve a folder object by its path in the reMarkable Cloud hierarchy
Parameters:
folder_path: Path to the folder starting with '/' for root (e.g., '/My Folder/Subfolder')
Returns: Folder object (Dict for REST API, Folder for rmcl) if found, None otherwise
async list_files_in_folder(self, folder_path: str, include_subfolders: bool = True) -> List[Union[Dict, Document]]
Purpose: List all PDF files in a folder and optionally its subfolders recursively
Parameters:
folder_path: Path to the folder to scaninclude_subfolders: If True, scan subfolders recursively, defaults to True
Returns: List of Document objects/dicts representing PDF files found in the folder
async download_document(self, document: Union[Dict, Document], output_dir: Path) -> Optional[Path]
Purpose: Download a document from reMarkable Cloud to local filesystem
Parameters:
document: Document object/dict to download (from list_files_in_folder)output_dir: Directory path where the downloaded file should be saved
Returns: Path object pointing to the downloaded file, or None if download failed
async upload_file(self, file_path: str, folder_path: str = None) -> Optional[str]
Purpose: Upload a file from local filesystem to reMarkable Cloud
Parameters:
file_path: Path to the local file to uploadfolder_path: Optional destination folder path in reMarkable Cloud, defaults to root
Returns: Document ID string if upload successful, None otherwise
async upload_content(self, content: bytes, filename: str, folder_path: str = None, file_type: str = 'application/pdf') -> Optional[str]
Purpose: Upload content directly from memory to reMarkable Cloud without saving to disk first
Parameters:
content: Bytes content to uploadfilename: Name for the file in reMarkable Cloudfolder_path: Optional destination folder path, defaults to rootfile_type: MIME type of the content, defaults to 'application/pdf'
Returns: Document ID string if upload successful, None otherwise
async create_folder(self, folder_path: str) -> bool
Purpose: Create a new folder in reMarkable Cloud at the specified path
Parameters:
folder_path: Full path to the folder to create (e.g., '/My Folder/New Subfolder')
Returns: True if folder created successfully or already exists, False otherwise
get_config(self) -> Dict
Purpose: Load configuration from the config file on disk
Returns: Dictionary containing configuration settings, empty dict if file doesn't exist or error occurs
save_config(self, config: Dict) -> None
Purpose: Save configuration dictionary to the config file on disk
Parameters:
config: Dictionary of configuration settings to persist
Returns: None - saves to disk or logs error if save fails
Attributes
| Name | Type | Description | Scope |
|---|---|---|---|
config_dir |
Path | Path object pointing to the configuration directory where tokens and settings are stored | instance |
device_token_file |
Path | Path to the file storing the reMarkable device authentication token | instance |
config_file |
Path | Path to the JSON configuration file | instance |
logger |
logging.Logger | Logger instance for recording errors and debug information | instance |
authenticated |
bool | Flag indicating whether the manager is currently authenticated with reMarkable Cloud | instance |
last_sync_time |
Optional[datetime] | Timestamp of the last synchronization operation, None if never synced | instance |
prefer_rest_api |
bool | Configuration flag indicating preference for REST API over rmcl library | instance |
rest_client |
RemarkableRestClient | Instance of the REST API client for reMarkable Cloud operations | instance |
rmcl_client |
Optional[Any] | Instance of the rmcl library client, None if not initialized or unavailable | instance |
client_type |
str | String indicating which client is currently active: 'rest' or 'rmcl' | instance |
folder_cache |
Dict[str, Dict] | Cache dictionary storing folder structure and metadata for REST API operations | instance |
file_cache |
Dict[str, Dict] | Cache dictionary storing file metadata for REST API operations | instance |
cache_timestamp |
Optional[datetime] | Timestamp when the cache was last populated, None if cache is invalid or empty | instance |
Dependencies
asyncioostempfiletimeuuiddatetimepathlibtypingloggingjsonremarkable_rest_clientrmcl
Required Imports
import asyncio
import os
import tempfile
import time
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Union
import logging
import json
from remarkable_rest_client import RemarkableRestClient, RemarkableRestFileWatcher
Conditional/Optional Imports
These imports are only needed under specific conditions:
from rmcl import Item, Document, Folder
Condition: only if rmcl library is installed and prefer_rest_api=False or as fallback when REST API fails
Optionalfrom rmcl.exceptions import ApiError, AuthError, DocumentNotFound, FolderNotFound
Condition: only if rmcl library is installed for exception handling
OptionalUsage Example
import asyncio
from pathlib import Path
from remarkable_cloud_manager import RemarkableCloudManager
async def main():
# Initialize manager with REST API preference
manager = RemarkableCloudManager(
config_dir='~/.eink-llm',
prefer_rest_api=True
)
# Authenticate (first time requires one-time code)
one_time_code = 'your-code-from-remarkable'
success = await manager.authenticate(one_time_code)
if not success:
print('Authentication failed')
return
# Create a folder
await manager.create_folder('/My Documents/LLM Output')
# List files in a folder
documents = await manager.list_files_in_folder('/My Documents', include_subfolders=True)
print(f'Found {len(documents)} documents')
# Download a document
if documents:
output_dir = Path('./downloads')
file_path = await manager.download_document(documents[0], output_dir)
if file_path:
print(f'Downloaded to {file_path}')
# Upload a file
doc_id = await manager.upload_file('document.pdf', '/My Documents')
if doc_id:
print(f'Uploaded with ID: {doc_id}')
# Upload content directly
with open('content.pdf', 'rb') as f:
content = f.read()
doc_id = await manager.upload_content(
content,
'my_document.pdf',
'/My Documents',
'application/pdf'
)
asyncio.run(main())
Best Practices
- Always call authenticate() before performing any operations - check the return value to ensure authentication succeeded
- Use async/await pattern for all method calls as most operations are asynchronous
- Handle None returns from methods gracefully - many methods return None on failure
- For first-time setup, obtain a one-time code from https://my.remarkable.com/connect/desktop
- The manager automatically handles fallback between REST API and rmcl - monitor console output for which client is being used
- Cache is invalidated automatically on folder creation - no manual cache management needed
- Document downloads may return .rm files for notebooks without original PDFs - handle both .pdf and .rm extensions
- Folder paths should start with '/' and use forward slashes - root folder is represented as '/' or empty string
- The manager maintains state (authenticated, client_type, caches) - reuse the same instance for multiple operations
- Check self.authenticated property before operations to verify authentication status
- Use include_subfolders=True in list_files_in_folder() for recursive scanning
- Output directories for downloads are created automatically if they don't exist
- Configuration and tokens are persisted to disk - subsequent runs may not need one-time code
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class RemarkableAPIClient 79.1% similar
-
class RemarkableUploadManager 75.4% similar
-
class RemarkableUploadManager_v1 74.8% similar
-
class Client 73.4% similar
-
class RemarkableFileWatcher 72.9% similar