-
Notifications
You must be signed in to change notification settings - Fork 8
/
__init__.py
1564 lines (1338 loc) · 58.6 KB
/
__init__.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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2010, Gregory Riker; 2014, Wulf C. Krueger <[email protected]>'
__docformat__ = 'restructuredtext en'
"""
Source for this plugin is available as a Github repository at
https://github.com/Philantrop/calibre-apple-reader-applications,
which also includes an overview of the communication protocol in README.md
"""
import base64, cStringIO, datetime, hashlib, imp, mechanize, os, platform, re, sqlite3, sys, tempfile, time
from collections import namedtuple
from inspect import getmembers, isfunction
from PIL import Image as PILImage
from threading import Thread
from types import MethodType
from calibre import browser, fit_image
from calibre.constants import cache_dir as _cache_dir, islinux, isosx, iswindows
#from calibre.devices.idevice.libimobiledevice import libiMobileDevice, libiMobileDeviceException
from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.books import CollectionsBookList
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.usbms.driver import debug_print
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, Tag
from calibre.ebooks.metadata import (author_to_author_sort, authors_to_string,
MetaInformation, title_sort)
from calibre.ebooks.metadata.epub import get_metadata, set_metadata
from calibre.ebooks.metadata.book.base import Metadata
from calibre.devices.errors import InitialConnectionError
from calibre.gui2.device import device_signals
from calibre.library import current_library_name
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config import config_dir, JSONConfig
from calibre.utils.zipfile import ZipFile
try:
from PyQt5.Qt import QDialog, QIcon, QObject, QPixmap, pyqtSignal
from PyQt5.uic import compileUi
except ImportError:
from PyQt4.Qt import QDialog, QIcon, QObject, QPixmap, pyqtSignal
from PyQt4.uic import compileUi
# Import glue from plugin if between calibre versions with glue updates
if False:
# To enable, bundle a current copy of libimobiledevice.py, parse_xml.py in iOS_reader_applications folder
# Disable import of XmlPropertyListParser in local copy of libimobiledevice.py (#24), replace with
# from calibre_plugins.ios_reader_apps.parse_xml import XmlPropertyListParser
from calibre_plugins.ios_reader_apps.libimobiledevice import libiMobileDevice, libiMobileDeviceException
else:
from calibre.devices.idevice.libimobiledevice import libiMobileDevice, libiMobileDeviceException
plugin_prefs = JSONConfig('plugins/iOS reader applications')
# #mark ~~~ READER_APP_ALIASES ~~~
# List of app names as installed by iOS. Prefix with 'b' for libiMobileDevice.
# These are the names that appear in the Config dialog for Preferred reader application
# 'iBooks' is removed under linux
# If an app is available in separate versions for iPad/iPhone, list iPad version first
READER_APP_ALIASES = {
'GoodReader': [b'com.goodiware.GoodReaderIPad', b'com.goodiware.GoodReader'],
'GoodReader 4': [b'com.goodiware.goodreader4'],
'iBooks': [b'com.apple.iBooks'],
'Kindle': [b'com.amazon.Lassen'],
'Marvin': [b'com.appstafarian.MarvinIP',
b'com.appstafarian.MarvinIP-free',
b'com.appstafarian.Marvin']
}
# Default format maps for Kindle options panel
KINDLE_ENABLED_FORMATS = ['MOBI', 'PDF']
KINDLE_SUPPORTED_FORMATS = ['MOBI', 'PDF']
class Logger():
'''
A self-modifying class to log debug statements.
If disabled in prefs, methods are neutered at first call for performance optimization
'''
LOCATION_TEMPLATE = "{cls}:{func}({arg1}) {arg2}"
def _log(self, msg=None):
'''
Upon first call, switch to appropriate method
'''
if not plugin_prefs.get('debug_plugin', False):
# Neuter the method
self._log = self.__null
self._log_location = self.__null
else:
# Log the message, then switch to real method
if msg:
debug_print(" {0}".format(str(msg)))
else:
debug_print()
self._log = self.__log
self._log_location = self.__log_location
def __log(self, msg=None):
'''
The real method
'''
if msg:
debug_print(" {0}".format(str(msg)))
else:
debug_print()
def _log_location(self, *args):
'''
Upon first call, switch to appropriate method
'''
if not plugin_prefs.get('debug_plugin', False):
# Neuter the method
self._log = self.__null
self._log_location = self.__null
else:
# Log the message from here so stack trace is valid
arg1 = arg2 = ''
if len(args) > 0:
arg1 = str(args[0])
if len(args) > 1:
arg2 = str(args[1])
debug_print(self.LOCATION_TEMPLATE.format(cls=self.__class__.__name__,
func=sys._getframe(1).f_code.co_name,
arg1=arg1, arg2=arg2))
# Switch to real method
self._log = self.__log
self._log_location = self.__log_location
def __log_location(self, *args):
'''
The real method
'''
arg1 = arg2 = ''
if len(args) > 0:
arg1 = str(args[0])
if len(args) > 1:
arg2 = str(args[1])
debug_print(self.LOCATION_TEMPLATE.format(cls=self.__class__.__name__,
func=sys._getframe(1).f_code.co_name,
arg1=arg1, arg2=arg2))
def __null(self, *args, **kwargs):
'''
Optimized method when logger is silent
'''
pass
class Book(Metadata):
'''
A simple class describing a book
See ebooks.metadata.book.base #46
'''
# 13 standard field keys from Metadata
iosra_standard_keys = ['author_sort', 'authors', 'comments', 'device_collections',
'pubdate', 'publisher', 'rating', 'series', 'series_index',
'tags', 'title', 'title_sort', 'uuid']
# 6 private field keys
iosra_custom_keys = ['cover_hash','datetime','description','path','size','thumbnail']
def __eq__(self, other):
all_mxd_keys = self.iosra_standard_keys + self.iosra_custom_keys
for attr in all_mxd_keys:
v1, v2 = [getattr(obj, attr, object()) for obj in [self, other]]
if v1 is object() or v2 is object():
return False
elif v1 != v2:
return False
return True
#def __init__(self, title, author):
# Metadata.__init__(self, title, authors=[author])
def __init__(self, *args, **kwargs):
if len(args) == 1:
title = args[0]
author = "Unknown"
elif len(args) == 2:
title, author = args
if 'authors' in kwargs:
authors = kwargs['authors']
else:
authors = [author]
Metadata.__init__(self, title, authors=authors)
def __ne__(self, other):
all_mxd_keys = self.iosra_standard_keys + self.iosra_custom_keys
for attr in all_mxd_keys:
v1, v2 = [getattr(obj, attr, object()) for obj in [self, other]]
if v1 is object() or v2 is object():
return True
elif v1 != v2:
return True
return False
@property
def title_sorter(self):
return title_sort(self.title)
class BookList(CollectionsBookList, Logger):
'''
A list of books.
Each Book object must have the fields:
1. title
2. authors
3. size (file size of the book)
4. datetime (a UTC time tuple)
5. path (path on the device to the book)
6. thumbnail (can be None) thumbnail is either a str/bytes object with the
image data or it should have an attribute image_path that stores an
absolute (platform native) path to the image
7. tags (a list of strings, can be empty).
'''
__getslice__ = None
__setslice__ = None
def __eq__(self, other):
all_mxd_keys = Book.iosra_standard_keys + Book.iosra_custom_keys
for x in range(len(self)):
for attr in all_mxd_keys:
v1, v2 = [getattr(obj, attr, None) for obj in [self[x], other[x]]]
if v1 is object() or v2 is object():
return False
elif v1 != v2:
return False
return True
def __init__(self, parent):
self.parent = parent
self.verbose = parent.verbose
#self._log_location()
def __ne__(self, other):
all_mxd_keys = Book.iosra_standard_keys + Book.iosra_custom_keys
for attr in all_mxd_keys:
v1, v2 = [getattr(obj, attr, object()) for obj in [self, other]]
if v1 is object() or v2 is object():
return True
elif v1 != v2:
return True
return False
def supports_collections(self):
''' Return True if the the device supports collections for this book list. '''
return True
def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal
metadata. Return True if booklists must be sync'ed
'''
self.append(book)
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the
same time
'''
self._log_location()
raise NotImplementedError()
def get_collections(self, collection_attributes):
'''
Return a dictionary of collections created from collection_attributes.
Each entry in the dictionary is of the form collection name:[list of
books]
The list of books is sorted by book title, except for collections
created from series, in which case series_index is used.
:param collection_attributes: A list of attributes of the Book object
'''
self._log_location()
return {}
def rebuild_collections(self, booklist, oncard):
'''
For each book in the booklist for the card oncard, remove it from all
its current collections, then add it to the collections specified in
device_collections.
oncard is None for the main memory, carda for card A, cardb for card B,
etc.
booklist is the object created by the :method:`books` call above.
This is called after the user edits the 'Collections' field in the Device view
when Metadata management is set to 'Manual'.
'''
self._log_location()
command_name = "rebuild_collections"
command_element = "rebuildcollections"
command_soup = BeautifulStoneSoup(self.parent.COMMAND_XML.format(
command_element, time.mktime(time.localtime())))
LOCAL_DEBUG = False
if booklist:
changed = 0
for book in booklist:
if LOCAL_DEBUG:
self._log("{0:7} {1}".format(book.in_library, book.title))
filename = self.parent.path_template.format(book.uuid)
if filename not in self.parent.cached_books:
for fn in self.parent.cached_books:
if book.uuid and book.uuid == self.parent.cached_books[fn]['uuid']:
if LOCAL_DEBUG:
self._log("'%s' matched on uuid %s" % (book.title, book.uuid))
filename = fn
break
elif (book.title == self.parent.cached_books[fn]['title'] and
book.authors == self.parent.cached_books[fn]['authors']):
if LOCAL_DEBUG:
self._log("'%s' matched on title/author" % book.title)
filename = fn
break
else:
self._log("ERROR: file %s not found in cached_books" % repr(filename))
continue
cached_collections = self.parent.cached_books[filename]['device_collections']
if cached_collections != book.device_collections:
# Append the changed book info to the command file
book_tag = Tag(command_soup, 'book')
book_tag['filename'] = filename
book_tag['title'] = book.title
book_tag['author'] = ', '.join(book.authors)
book_tag['uuid'] = book.uuid
collections_tag = Tag(command_soup, 'collections')
for tag in book.device_collections:
c_tag = Tag(command_soup, 'collection')
c_tag.insert(0, tag)
collections_tag.insert(0, c_tag)
book_tag.insert(0, collections_tag)
command_soup.manifest.insert(0, book_tag)
# Update cache
self.parent.cached_books[filename]['device_collections'] = book.device_collections
changed += 1
if changed:
# Stage the command file
self.parent._stage_command_file(command_name, command_soup,
show_command=self.parent.prefs.get('development_mode', False))
# Wait for completion
self.parent._wait_for_command_completion(command_name)
else:
self._log("no collection changes detected cached_books <=> device books")
"""
def _log(self, msg=None):
'''
Print msg to console
'''
if not self.verbose:
return
if msg:
debug_print(" %s" % msg)
else:
debug_print()
def _log_location(self, *args):
'''
Print location, args to console
'''
if not self.verbose:
return
arg1 = arg2 = ''
if len(args) > 0:
arg1 = args[0]
if len(args) > 1:
arg2 = args[1]
debug_print(self.LOCATION_TEMPLATE.format(cls=self.__class__.__name__,
func=sys._getframe(1).f_code.co_name,
arg1=arg1, arg2=arg2))
"""
class CompileUI():
'''
Compile Qt Creator .ui files at runtime
'''
def __init__(self, parent):
self.compiled_forms = {}
self.help_file = None
self._log = parent._log
self._log_location = parent._log_location
self.parent = parent
self.verbose = parent.verbose
self.compiled_forms = self.compile_ui()
def compile_ui(self):
pat = re.compile(r'''(['"]):/images/([^'"]+)\1''')
def sub(match):
ans = 'I(%s%s%s)'%(match.group(1), match.group(2), match.group(1))
return ans
# >>> Entry point
self._log_location()
compiled_forms = {}
self._find_forms()
# Cribbed from gui2.__init__:build_forms()
for form in self.forms:
with open(form) as form_file:
soup = BeautifulStoneSoup(form_file.read())
property = soup.find('property',attrs={'name' : 'windowTitle'})
string = property.find('string')
window_title = string.renderContents()
compiled_form = self._form_to_compiled_form(form)
if (not os.path.exists(compiled_form) or
os.stat(form).st_mtime > os.stat(compiled_form).st_mtime):
if not os.path.exists(compiled_form):
if self.verbose:
self._log(' compiling %s' % form)
else:
if self.verbose:
self._log(' recompiling %s' % form)
os.remove(compiled_form)
buf = cStringIO.StringIO()
compileUi(form, buf)
dat = buf.getvalue()
dat = dat.replace('__appname__', 'calibre')
dat = dat.replace('import images_rc', '')
dat = re.compile(r'(?:QtGui.QApplication.translate|(?<!def )_translate)\(.+?,\s+"(.+?)(?<!\\)",.+?\)').sub(r'_("\1")', dat)
dat = dat.replace('_("MMM yyyy")', '"MMM yyyy"')
dat = pat.sub(sub, dat)
with open(compiled_form, 'wb') as cf:
cf.write(dat)
compiled_forms[window_title] = compiled_form.rpartition(os.sep)[2].partition('.')[0]
return compiled_forms
def _find_forms(self):
forms = []
for root, _, files in os.walk(self.parent.resources_path):
for name in files:
if name.endswith('.ui'):
forms.append(os.path.abspath(os.path.join(root, name)))
self.forms = forms
def _form_to_compiled_form(self, form):
compiled_form = form.rpartition('.')[0]+'_ui.py'
return compiled_form
class DatabaseMalformedException(Exception):
''' '''
pass
class DatabaseNotFoundException(Exception):
''' '''
pass
class DriverBase(DeviceConfig, DevicePlugin):
# Specified at runtime in settings()
FORMATS = []
def config_widget(self):
'''
See devices.usbms.deviceconfig:DeviceConfig()
'''
self._log_location()
from calibre_plugins.ios_reader_apps.config import ConfigWidget
applist = READER_APP_ALIASES.keys()
if islinux and 'iBooks' in applist:
applist.remove('iBooks')
if isosx and platform.mac_ver()[0] >= "10.9":
if not plugin_prefs.get('ibooks_override', False):
self._log("*** iBooks is not supported > OS X 10.8 ***")
applist.remove('iBooks')
else:
self._log("*** ibooks_override enabled under OS X {0} ***".format(platform.mac_ver()[0]))
self.cw = ConfigWidget(self, applist)
return self.cw
def save_settings(self, config_widget):
self._log_location()
config_widget.save_settings()
class InvalidEpub(ValueError):
pass
class iOSReaderApp(DriverBase, Logger):
'''
'''
# Flow of control when device recognized
'''
Launch:
__init__()
initialize()
After GUI displayed:
startup()
is_usb_connected()
can_handle() --or-- detect_managed_devices() depending on MANAGES_DEVICE_PRESENCE
reset()
open()
card_prefix()
set_progress_reporter()
get_device_information()
card_prefix()
IF Automatic metadata management:
sync_booklists()
free_space()
set_progress_reporter()
books()
Upload books to device:
set_progress_reporter()
set_plugboards()
upload_books()
add_books_to_metadata()
set_plugboards()
set_progress_reporter()
sync_booklists()
card_prefix()
free_space()
Delete book from device:
delete_books()
remove_books_from_metadata()
set_plugboards()
set_progress_reporter()
sync_booklists()
card_prefix()
free_space()
Get book from device:
prepare_addable_books()
get_file()
set_plugboards()
set_progress_reporter()
sync_booklists()
card_prefix()
free_space()
'''
app_id = None
author = 'Wulf C. Krueger'
books_subpath = None
description = 'Communicate with Apple iOS reader applications'
device_profile = None
ejected = None
format_map = []
gui_name = 'iOS reader applications'
icon = None
name = 'iOS reader applications'
overlays_loaded = False
supported_platforms = ['linux', 'osx', 'windows']
temp_dir = None
verbose = None
# #mark ~~~ plugin version, minimum calibre version ~~~
version = (1, 4, 7, 0, 0)
minimum_calibre_version = (1, 37, 0)
# #mark ~~~ USB fingerprints ~~~
# Init the BCD and USB fingerprints sets
_PRODUCT_ID = set([])
_BCD = set([0x01])
''' iPad '''
if True:
_PRODUCT_ID.add(0x129a) # iPad1 (can't be upgraded past 5.x)
_PRODUCT_ID.add(0x129f) # iPad2 WiFi
_BCD.add(0x210)
_PRODUCT_ID.add(0x12a2) # iPad2 GSM
_BCD.add(0x220)
_PRODUCT_ID.add(0x12a3) # iPad2 CDMA
_BCD.add(0x230) # Verizon
_PRODUCT_ID.add(0x12a9) # iPad2 WiFi (2nd iteration)
_BCD.add(0x240)
_PRODUCT_ID.add(0x12a4) # iPad3 WiFi
_BCD.add(0x310)
_PRODUCT_ID.add(0x12a5) # iPad3 CDMA (Verizon)
_BCD.add(0x320)
_PRODUCT_ID.add(0x12a6) # iPad3 GSM
_BCD.add(0x330)
_PRODUCT_ID.add(0x12ab) # iPad4
_BCD.add(0x340) # WiFi
_BCD.add(0x350) # GSM (ME401LL/A)
_BCD.add(0x360) # GSM (Telstra AU)
_BCD.add(0x401) # iPad Air WiFi
_BCD.add(0x402) # iPad Air GSM
_BCD.add(0x503) # iPad Air 2 WiFi
_BCD.add(0x504) # iPad Air 2 GSM
''' iPad Mini (_PRODUCT_ID 0x12ab shared with iPad) '''
if True:
_BCD.add(0x250) # iPad Mini WiFi
_BCD.add(0x260) # iPad Mini GSM LTE Rogers (Canada)
_BCD.add(0x270) # iPad Mini GSM ???
_BCD.add(0x404) # iPad rMini WiFi
_BCD.add(0x405) # iPad rMini GSM (Verizon LTE)
_BCD.add(0x407) # iPad Mini 3 WiFi
_BCD.add(0x408) # iPad Mini 3 GSM ???
''' iPhone '''
if True:
_PRODUCT_ID.add(0x1292) # iPhone3G
_PRODUCT_ID.add(0x1294) # iPhone3GS
_PRODUCT_ID.add(0x1297) # iPhone4 (Telus)
_BCD.add(0x310)
_PRODUCT_ID.add(0x129c) # iPhone4 (Verizon)
_BCD.add(0x330)
_PRODUCT_ID.add(0x12a0) # iPhone4S GSM
_BCD.add(0x410)
_PRODUCT_ID.add(0x12a8) # iPhone5 GSM
_BCD.add(0x510)
_BCD.add(0x520) # GSM (Telefonica-Movistar Spain)
_BCD.add(0x530) # 5C (Softbank Japan)
_BCD.add(0x540) # 5C (???)
_BCD.add(0x601) # iPhone 5S (AT&T)
_BCD.add(0x602) # iPhone 5S (Telstra)
_BCD.add(0x701) # iPhone 6 Plus
_BCD.add(0x702) # iPhone 6
_BCD.add(0x802) # iPhone 6S Plus
''' iPod '''
if True:
_PRODUCT_ID.add(0x1291) # iPod Touch
_PRODUCT_ID.add(0x1293) # iPod Touch 2G
_PRODUCT_ID.add(0x1299) # iPod Touch 3G
_PRODUCT_ID.add(0x129e) # iPod Touch 4G
_BCD.add(0x410)
_PRODUCT_ID.add(0x12aa) # iPod Touch 5G
_BCD.add(0x510)
# Finalize the supported BCD and USB fingerprints
VENDOR_ID = [0x05ac]
PRODUCT_ID = list(_PRODUCT_ID)
BCD = list(_BCD)
@property
def archive_path(self):
return os.path.join(self.cache_dir, "thumbs.zip")
@property
def cache_dir(self):
return os.path.join(_cache_dir(), self.ios_reader_app)
def books(self, oncard=None, end_session=True):
'''
Return a list of ebooks on the device.
@param oncard: If 'carda' or 'cardb' return a list of ebooks on the
specific storage card, otherwise return list of ebooks
in main memory of device. If a card is specified and no
books are on the card return empty list.
@return: A BookList.
'''
raise NotImplementedError()
def can_handle(self, device_info, debug=False):
'''
If calibre was started without an iDevice connected, control will come here
upon connection.
Perform a late overlay binding, then pass control to overlay can_handle()
'''
self._log_location("overlays_loaded: %s" % self.overlays_loaded)
# If reader app specified, check for installation, reconfigure
if self.ios_reader_app:
self.app_id = self._get_connected_device_info()
if self.app_id is not None and not self.overlays_loaded:
# Device connected, app installed
self._log("performing late overlay binding")
self._class_reconfigure()
self.overlays_loaded = True
else:
if self.ios_reader_app is not None:
self._log_location("Preferred iOS reader app '%s' not installed" % self.ios_reader_app)
return False
def card_prefix(self, end_session=True):
'''
Return a 2 element list of the prefix to paths on the cards.
If no card is present None is set for the card's prefix.
E.G.
('/place', '/place2')
(None, 'place2')
('place', None)
(None, None)
'''
return (None, None)
def free_space(self, end_session=True):
'''
Return available space on device
self.device_profile initialized during get_device_information()
'''
self._log_location()
try:
available_space = long(self.device_profile['FSFreeBytes'])
return (available_space, -1, -1)
except:
return(-1, -1, -1)
def get_device_information(self, end_session=True):
'''
Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type)
'''
self._log_location()
# self.device_profile built in initialize(), this is the full code for reference
if False:
self._log("getting device profile late")
self.ios.connect_idevice()
preferences = self.ios.get_preferences()
self.ios.disconnect_idevice()
self.ios.mount_ios_media_folder()
device_info = self.ios._afc_get_device_info()
self.ios.dismount_ios_media_folder()
device_info.pop('Model')
self.device_profile = dict(preferences.items() + device_info.items())
if True and self.verbose:
for item in sorted(self.device_profile):
if item in ['FSTotalBytes', 'FSFreeBytes']:
self._log(" {0:21}: {1:,}".format(item, int(self.device_profile[item])))
else:
self._log(" {0:21}: {1}".format(item, self.device_profile[item]))
device_information = (self.device_profile['DeviceName'],
self.device_profile['ProductType'],
self.device_profile['ProductVersion'],
'unknown mime type')
return device_information
def get_option(self):
self._log_location()
def initialize(self):
'''
Copy the iOS Reader App icons to our resource folder
Copy the iOS Reader App config widgets to our resource folder
Init the JSON prefs file
'''
self.prefs = plugin_prefs
self.verbose = self.prefs.get('debug_plugin', False)
self._log_location("v%d.%d.%d.%d.%d" % self.version)
self.resources_path = os.path.join(config_dir, 'plugins', "%s_resources" % self.name.replace(' ', '_'))
# ~~~~~~~~~ Copy the icon files to our resource directory ~~~~~~~~~
icons = []
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate.endswith('/'):
continue
if candidate.startswith('icons/'):
icons.append(candidate)
ir = self.load_resources(icons)
for icon in icons:
if not icon in ir:
continue
fs = os.path.join(self.resources_path, icon)
if not os.path.exists(fs):
if not os.path.exists(os.path.dirname(fs)):
os.makedirs(os.path.dirname(fs))
with open (fs, 'wb') as f:
f.write(ir[icon])
else:
# Is the icon file current?
update_needed = False
with open(fs, 'rb') as f:
if f.read() != ir[icon]:
update_needed = True
if update_needed:
with open(fs, 'wb') as f:
f.write(ir[icon])
# ~~~~~~~~~ Copy the widget files to our resource directory ~~~~~~~~~
widgets = []
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
# Qt UI files
if candidate.startswith('widgets/') and candidate.endswith('.ui'):
widgets.append(candidate)
# Corresponding class definitions
if candidate.startswith('widgets/') and candidate.endswith('.py'):
widgets.append(candidate)
wr = self.load_resources(widgets)
for widget in widgets:
if not widget in wr:
continue
fs = os.path.join(self.resources_path, widget)
if not os.path.exists(fs):
# If the file doesn't exist in the resources dir, add it
if not os.path.exists(os.path.dirname(fs)):
os.makedirs(os.path.dirname(fs))
with open (fs, 'wb') as f:
f.write(wr[widget])
else:
# Is the .ui file current?
update_needed = False
with open(fs, 'r') as f:
if f.read() != wr[widget]:
update_needed = True
if update_needed:
with open (fs, 'wb') as f:
f.write(wr[widget])
# ~~~~~~~~~ Copy the help files to our resource directory ~~~~~~~~~
help_files = []
with ZipFile(self.plugin_path, 'r') as zf:
for candidate in zf.namelist():
if candidate.startswith('help/') and candidate.endswith('.html'):
help_files.append(candidate)
hrs = self.load_resources(help_files)
for hf in help_files:
if not hf in hrs:
continue
fs = os.path.join(self.resources_path, hf)
if not os.path.exists(fs):
# If the file doesn't exist in the resources dir, add it
if not os.path.exists(os.path.dirname(fs)):
os.makedirs(os.path.dirname(fs))
with open (fs, 'wb') as f:
f.write(hrs[hf])
else:
# Is the help file current?
update_needed = False
with open(fs, 'r') as f:
if f.read() != hrs[hf]:
update_needed = True
if update_needed:
with open (fs, 'wb') as f:
f.write(hrs[hf])
# Compile .ui files as needed
cui = CompileUI(self)
# Init the prefs file as needed
self._init_prefs()
if getattr(self, 'temp_dir') is None:
iOSReaderApp._create_temp_dir('_ios_local_db')
# Init libiMobileDevice
self.ios = libiMobileDevice(verbose=self.prefs.get('debug_libimobiledevice', False))
# Confirm the installation of the preferred reader app
self.app_id = None
# Special case development overlay
if (self.prefs.get('development_mode', False) and
self.prefs.get('development_overlay', None) and
self.prefs.get('development_app_id', None)):
self.app_id = self.prefs.get('development_app_id', None)
self.ios_reader_app = "development_mode"
self._get_connected_device_info()
else:
self.ios_reader_app = self.prefs.get('preferred_reader_app', None)
if self.ios_reader_app:
self.app_id = self._get_connected_device_info()
else:
self._log("No preferred reader app selected in config")
self.ios_reader_app = None
# Device connected, app installed
if self.app_id is not None and self.ios_reader_app is not None:
self.ejected = False
self._class_reconfigure()
def is_running(self):
self._log_location()
def is_usb_connected(self, devices_on_system, debug=False, only_presence=False):
ans = super(iOSReaderApp, self).is_usb_connected(devices_on_system, debug, only_presence)
#self._log_location(ans)
return ans
def is_usb_connected_windows(self, devices_on_system, debug=False, only_presence=False):
'''
If calibre was started without an iDevice connected, control will come here
with ans[0] = True when the device connects
Perform a late overlay binding, mount the preferred reader app, then pass
control to the overlays.
Always return False, None, as the overlaid version will take over next time
this method is called
'''
ans = super(iOSReaderApp, self).is_usb_connected_windows(devices_on_system, debug, only_presence)
usb_connected = ans[0]
#self._log_location(ans)
if usb_connected:
# If reader app specified, check for installation, reconfigure
if self.ios_reader_app:
self.app_id = self._get_connected_device_info()
if self.app_id is not None and not self.overlays_loaded:
# Device connected, app installed
self._log("performing late overlay binding")
self._class_reconfigure()
self.overlays_loaded = True
# Unique to Windows - need to connect to app folder before continuing
self.ios_connection['app_installed'] = self.ios.mount_ios_app(app_id=self.app_id)
self.ios_connection['device_name'] = self.ios.device_name
else:
self._log("device connected, but no reader app selected")
return False, None
def open(self, connected_device, library_uuid):
'''
If the user has selected iBooks as the preferred iOS reader app,
morph to class ITUNES, initialize and launch iTunes in can_handle(),
call ITUNES:open(), and return. All subsequent driver calls will be
handled by ITUNES driver.
ITUNES.verbose is supplied from our debug_plugin value.
Otherwise, load the class overlay methods for preferred iOS reader app,
create/confirm thumbs archive.
*** NB: Do not overwrite this method in reader class! ***
self.vid, self.pid referenced in is_usb_connected_windows to determine when
ejected device has been physically removed from the system
'''
self._log_location()
self.vid = connected_device[0]
self.pid = connected_device[1]
self._log(" Vendor ID (vid):%04x Product ID: (pid):%04x" % (self.vid, self.pid))
def reset(self, **kwargs):
'''
:key: The key to unlock the device
:log_packets: If true the packet stream to/from the device is logged
:report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks
If it is called with -1 that means that the
task does not have any progress information
:detected_device: Device information from the device scanner
'''
self._log_location()
if False:
for key, value in kwargs.iteritems():
self._log("%s = %s" % (key, value))
else:
pass