-
Notifications
You must be signed in to change notification settings - Fork 7
/
api.py
executable file
·179 lines (138 loc) · 6.04 KB
/
api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
'''
Utilities regarding the reMarkable usb webinterface.
'''
from collections.abc import Iterable
from datetime import datetime
import requests
RM_WEB_UI_URL = 'http://10.11.99.1' # No trailing slash!
class RmFile:
'''
Representation of a file or folder on the reMarkable device.
'''
def __init__(self, metadata, parent=None):
# Given parameters:
self.metadata = metadata
self.parent = parent
# Hierachial data:
self.isFolder = (metadata['Type'] == 'CollectionType')
self.files = [] if self.isFolder else None
# Determine file type:
self.isNotebook = not self.isFolder and metadata['fileType'] == 'notebook'
self.isPdf = not self.isFolder and metadata['fileType'] == 'pdf'
self.isEpub = not self.isFolder and metadata['fileType'] == 'epub'
# Easy access of common metadata:
self.name = metadata['VissibleName'] if 'VissibleName' in metadata else metadata['VisibleName'] # Typo from reMarkable which will probably get fixed in next patch
self.id = metadata['ID']
self.isBookmarked = metadata['Bookmarked']
self.pages = metadata['pageCount'] if not self.isFolder else None
self.modifiedTimestamp = datetime.strptime(metadata['ModifiedClient'], '%Y-%m-%dT%H:%M:%S.%fZ').timestamp()
# Prevent faulty structures:
if '/' in self.name:
self.name = self.name.replace('/', '')
def path(self, basePath=''):
'''
Returns the complete path including this file/folder.
basePath may be provided and prepended.
'''
if basePath and not basePath.endswith('/'):
basePath += '/'
path = self.name
parent = self.parent
while parent:
path = parent.name + '/' + path
parent = parent.parent
return basePath + path
def parentFolderPath(self, basePath=''):
'''
Returns the current folder path that a file is in.
A basePath may be provided and prepended.
If in root, the basePath (default '') will be returned.
'''
if self.parent:
if basePath and not basePath.endswith('/'):
basePath += '/'
return basePath + self.parent.path()
else:
basePath
def exportPdf(self, targetPath):
'''
Downloads this file as pdf to a given path.
I may take a while before the download is started,
due to the conversion happening on the reMarkable device.
Returns bool with True for successful and False for unsuccessful.
'''
response = requests.get(RM_WEB_UI_URL + '/download/' + self.id + '/placeholder', stream=True)
if not response.ok:
return False
# Credit: https://stackoverflow.com/a/13137873
response.raw.decode_content = True # Decompress if needed
with open(targetPath, 'wb') as targetFile:
for chunk in response.iter_content(8192):
targetFile.write(chunk)
return True
def __str__(self):
return 'RmFile{name=%s}' % self.name
def __repr__(self):
return self.__str__()
def iterateAll(filesOrRmFile):
'''
Yields all files in this iterable (list, tuple, ...) or file including subfiles and folders.
In case any (nested) value that isn't a iterable or of class api.RmFile a ValueError will be raised!
'''
if isinstance(filesOrRmFile, RmFile):
# Yield file and optional sub files recursivly:
yield filesOrRmFile
if filesOrRmFile.isFolder:
for recursiveSubFile in iterateAll(filesOrRmFile.files):
yield recursiveSubFile
elif isinstance(filesOrRmFile, Iterable):
# Assumes elements to be of class api.RmFile
for rmFile in filesOrRmFile:
for subFile in iterateAll(rmFile):
yield subFile
else:
# Unknown type found!
raise ValueError('"api.iterateAll(filesOrRmFile)" only accepts (nested) iterables and instances of class api.RmFile !')
def fetchFileStructure(parentRmFile=None):
'''
Fetches the fileStructure from the reMarkable USB webinterface.
Specify a RmFile of type folder to fetch only all folders after the given one.
Ignore to get all possible file and folders.
Returns either list of files OR FILLS given parentRmFiles files
May throw RuntimeError or other releated ones from "requests"
if there are problems fetching the data from the usb webinterface.
'''
if parentRmFile and not parentRmFile.isFolder:
raise ValueError('"api.fetchFileStructure(parentRmFile)": parentRmFile must be None or of type folder!')
# Use own list if not parentRmFile is given (aka beeing in root)
if not parentRmFile:
rootFiles = []
# Get metadata:
directoryMetadataUrl = RM_WEB_UI_URL + '/documents/' + (parentRmFile.id if parentRmFile else '')
response = requests.get(directoryMetadataUrl)
response.encoding = 'UTF-8' # Ensure, all Non-ASCII characters get decoded properly
if not response.ok:
raise RuntimeError('Url %s responsed with status code %d' % (directoryMetadataUrl, response.status_code))
directoryMetadata = response.json()
# Parse Entries in jsonArray (= files in directory) and request subFolders:
for fileMetadata in directoryMetadata:
rmFile = RmFile(fileMetadata, parentRmFile)
if parentRmFile:
parentRmFile.files.append(rmFile)
else:
rootFiles.append(rmFile)
# Fetch subdirectories recursivly:
if rmFile.isFolder:
fetchFileStructure(rmFile)
# Return files as list if in root (otherwise the given parentRmFile got their files):
if not parentRmFile:
return rootFiles
def findId(filesOrRmFile, fileId):
'''
Searched for any file or directory with fileId in given filesOrRmFile (including nested files)
Returns matching RmFile if found. Otherwise None .
'''
for rmFile in iterateAll(filesOrRmFile):
if rmFile.id == fileId:
return rmFile
return None