summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Étienne Deparis 2020-01-27 20:31:52 +0100
committerGravatar Étienne Deparis 2020-01-27 22:37:15 +0100
commitb543d9fa94ef9e36e06d7cbeb37e3b2f03cc6042 (patch)
treeead2395d195fc0b17f940d600bf80813adfefe19
parentb1017fbf78e7cec8a7859158ae6567cbdd89d492 (diff)
downloadeshelf-b543d9fa94ef9e36e06d7cbeb37e3b2f03cc6042.tar.gz
eshelf-b543d9fa94ef9e36e06d7cbeb37e3b2f03cc6042.tar.xz
eshelf-b543d9fa94ef9e36e06d7cbeb37e3b2f03cc6042.zip
Major refactoring to adopt modern python guidelines
-rw-r--r--eshelf.py466
-rw-r--r--eshelf/__init__.py2
-rw-r--r--eshelf/backup.py (renamed from plugins/backup.py)3
-rw-r--r--eshelf/bookeen.py40
-rw-r--r--eshelf/cli.py234
-rw-r--r--eshelf/reader_orm.py106
-rw-r--r--eshelf/setup.py47
-rw-r--r--eshelf/utils.py45
-rw-r--r--setup.cfg24
-rw-r--r--setup.py2
10 files changed, 500 insertions, 469 deletions
diff --git a/eshelf.py b/eshelf.py
deleted file mode 100644
index e41f41e..0000000
--- a/eshelf.py
+++ /dev/null
@@ -1,466 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-import os
-import re
-import sys
-import sqlite3
-import argparse
-from configparser import ConfigParser
-from xdg.BaseDirectory import xdg_config_home
-
-
-def file_listing_helper(current_dir, padding=None):
- if padding is None:
- padding = ''
-
- decorator = '├──'
- decorator_prefix = padding + '│ '
-
- curfilelist = os.listdir(current_dir)
- curfilelistlen = len(curfilelist) - 1
- curpointer = 0
- for curfile in curfilelist:
- if curpointer == curfilelistlen:
- decorator = '└──'
- decorator_prefix = padding + ' '
-
- curpointer += 1
- print('{0}{1} {2}'.format(padding, decorator, curfile))
-
- curfile_path = os.path.join(current_dir, curfile)
- if os.path.isdir(curfile_path):
- file_listing_helper(curfile_path, decorator_prefix)
-
-
-def read_file(f):
- if os.path.isfile(f):
- with open(f, 'r') as s:
- return s.read().strip()
- return ''
-
-
-def bookeen_identify():
- import subprocess
- try:
- usblist = str(subprocess.check_output(['lsusb']))
- except subprocess.CalledProcessError:
- usblist = ''
-
- vendorlist = ['0525:a4a5', '0bda:0703', '3034:1795']
- for prefix in vendorlist:
- if usblist.find(prefix) >= 0:
- if prefix == '0525:a4a5':
- return 'Bookeen Cybook Muse'
-
- try:
- usbdetails = str(subprocess.check_output(
- ['lsusb', '-d{0}'.format(prefix), '-v']))
- m = re.search('bcdDevice\s*([0-9.]+)\\\\n', usbdetails)
- if m is not None and m.group(1) == '3.19':
- return 'Bookeen Cybook Orizon'
-
- except subprocess.CalledProcessError:
- pass
-
- return 'Bookeen Cybook Opus or Gen 3'
-
- return None
-
-
-class Shelf:
- def __init__(self, library_path):
- if library_path is None:
- return
-
- if not os.path.isdir(library_path):
- print('{0} does not exists, please provide us a valid path.'
- .format(library_path))
- sys.exit(1)
-
- self.library_path = library_path
- config_file = os.path.join(self.library_path, '.eshelf.conf')
- if not os.path.isfile(config_file):
- print('{0} is not a valid library folder. Please do first'
- ' `eshelf init`'.format(self.library_path))
- sys.exit(1)
-
- confparser = ConfigParser()
- confparser.read(config_file)
- self.config = confparser['DEFAULT']
-
- if 'reader_mount_point' not in self.config:
- print('{0} has been corrupted. Please redo'
- ' `eshelf init`'.format(self.library_path))
- sys.exit(1)
-
- self.reader_mount_point = self.config['reader_mount_point']
- self.reader_db_path = os.path.join(
- self.reader_mount_point, 'system', 'library')
- self.reader_db = None
-
- self.remote_physical_path = '/mnt/fat'
- if 'remote_physical_path' in self.config:
- self.remote_physical_path = self.config['remote_physical_path']
-
- def get_config(self):
- return self.config
-
- def get_reader_db(self):
- if self.reader_db is None:
- try:
- self.reader_db = sqlite3.connect(self.reader_db_path)
- self.reader_db.row_factory = sqlite3.Row
- self.reader_db.isolation_level = None
-
- except sqlite3.OperationalError:
- print('Something wrong happen. Maybe you do not have'
- ' mounted your ereader.')
- sys.exit(1)
-
- return self.reader_db
-
- def get_cursor(self):
- return self.get_reader_db().cursor()
-
- def collections(self, opts=[]):
- finaldata = []
- cur = self.get_cursor()
- for row in cur.execute('select f_id_shelf, f_name, f_readonly'
- ' from t_shelf order by f_name'):
- loc_data = dict(row)
- if loc_data['f_readonly'] == 0:
- finaldata.append('[{0}] {1}'.format(
- loc_data['f_id_shelf'], loc_data['f_name']))
-
- if len(finaldata) == 0:
- return
-
- print('Collections:')
- decorator = '├──'
- curfilelistlen = len(finaldata) - 1
- curpointer = 0
- for line in finaldata:
- if curpointer == curfilelistlen:
- decorator = '└──'
-
- curpointer += 1
- print('{0} {1}'.format(decorator, line))
-
- def books(self, opts=[]):
- cur = self.get_cursor()
- finaldata = []
- values = ()
- shelf_name = None
- request = None
- orphans = False
- if len(opts) == 1:
- if opts[0] == 'orphans':
- orphans = True
-
- else:
- if str(opts[0]).isdecimal():
- shelf_infos_req = 'select f_id_shelf, f_name ' \
- 'from t_shelf where f_id_shelf = ?'
- else:
- shelf_infos_req = 'select f_id_shelf, f_name ' \
- 'from t_shelf where f_name = ? collate nocase'
-
- shelf_infos = cur.execute(
- shelf_infos_req, (opts[0],)).fetchone()
- if shelf_infos is None:
- print('No such shelf {0}'.format(opts[0]))
- return
-
- shelf_infos = dict(shelf_infos)
- shelf_name = shelf_infos['f_name']
- values = (shelf_infos['f_id_shelf'],)
- request = 'select b.f_id_item, b.f_title, b.f_internal_uri ' \
- 'from t_item b, t_shelf_link tb where ' \
- 'tb.f_item_id = b.f_id_item and tb.f_shelf_id = ? ' \
- 'order by b.f_title'
-
- if request is None:
- request = 'select f_id_item, f_title, f_internal_uri ' \
- 'from t_item order by f_title'
-
- for row in cur.execute(request, values):
- loc_data = dict(row)
- real_internal_path = re.sub(
- self.remote_physical_path + '/',
- self.reader_mount_point,
- loc_data['f_internal_uri'])
-
- local_file_path = re.sub(
- os.path.join(
- self.remote_physical_path,
- self.config['remote_library_path']) + '/',
- '',
- loc_data['f_internal_uri'])
- # Twice to be sure to coverage all cases
- local_file_path = re.sub(
- self.remote_physical_path + '/',
- '',
- local_file_path)
-
- if orphans:
- if not os.path.isfile(
- os.path.join(self.library_path, local_file_path)):
- local_file = real_internal_path
- else:
- continue
- else:
- local_file = '(not on your computer: {0})' \
- .format(local_file_path)
- if os.path.isfile(os.path.join(
- self.library_path, local_file_path)):
- local_file = ''
-
- if os.path.isfile(real_internal_path):
- finaldata.append('[{0}] {1} {2}'.format(
- loc_data['f_id_item'], loc_data['f_title'], local_file))
-
- if shelf_name is not None:
- print('Books in {0}:'.format(shelf_name))
- else:
- print('Books in your eReader library:')
-
- if len(finaldata) == 0:
- return
-
- decorator = '├──'
- curfilelistlen = len(finaldata) - 1
- curpointer = 0
- for line in finaldata:
- if curpointer == curfilelistlen:
- decorator = '└──'
-
- curpointer += 1
- print('{0} {1}'.format(decorator, line))
-
- def prepare_add_remove(self, shelf_id_or_name, book_id_or_title):
- cur = self.get_cursor()
-
- # Ok now try to find corresponding shelf
- if str(shelf_id_or_name).isdecimal():
- shelf_infos_req = 'select f_id_shelf from t_shelf ' \
- 'where f_id_shelf = ?'
- else:
- shelf_infos_req = 'select f_id_shelf from t_shelf ' \
- 'where f_name = ? collate nocase'
-
- shelf_infos = cur.execute(
- shelf_infos_req, (shelf_id_or_name,)).fetchone()
- if shelf_infos is None:
- print('No such shelf {0}'.format(shelf_id_or_name))
- sys.exit(1)
- shelf_infos = dict(shelf_infos)
-
- # Now check if book exists
- if str(book_id_or_title).isdecimal():
- book_infos_req = 'select f_id_item from t_item ' \
- 'where f_id_item = ?'
- else:
- book_infos_req = 'select f_id_item from t_item ' \
- 'where f_title = ? collate nocase'
-
- book_infos = cur.execute(
- book_infos_req, (book_id_or_title,)).fetchone()
- if book_infos is None:
- print('No such book {0}'.format(book_id_or_title))
- sys.exit(1)
- book_infos = dict(book_infos)
-
- # Internal requirements
- b_uuid = dict(
- cur.execute('select f_db_uuid from t_uuiddb limit 1').fetchone())
-
- return shelf_infos['f_id_shelf'], book_infos['f_id_item'], \
- b_uuid['f_db_uuid']
-
- def add(self, opts=[]):
- if len(opts) < 2:
- print('Action must follow the form: add <id_or_title>'
- ' <shelf_id_or_name>')
- sys.exit(1)
-
- cur_shelf, cur_book, b_uuid = self.prepare_add_remove(opts[1], opts[0])
-
- cur = self.get_cursor()
- cur.execute(
- "insert into t_shelf_link values (?, ?, ?)",
- (cur_book, cur_shelf, b_uuid))
- print('Successfully added a book to a shelf.')
- self.books([cur_shelf])
-
- def remove(self, opts=[]):
- if len(opts) < 2:
- print('Action must follow the form: remove <id_or_title>'
- ' <shelf_id_or_name>')
- sys.exit(1)
-
- cur_shelf, cur_book, b_uuid = self.prepare_add_remove(opts[1], opts[0])
-
- cur = self.get_cursor()
- cur.execute(
- "delete from t_shelf_link where f_item_id = ?"
- " and f_shelf_id = ? and f_db_uuid = ?",
- (cur_book, cur_shelf, b_uuid))
- print('Successfully removed a book to a shelf.')
- self.books([cur_shelf])
-
- def edit(self, opts=[]):
- if len(opts) < 2:
- print('Action must follow the form: edit <id> <new_title>')
- sys.exit(1)
-
- # Now check if book exists
- if str(opts[0]).isdecimal():
- book_infos_req = 'select f_id_item from t_item where f_id_item = ?'
- else:
- print('Book id not recognized. Are you sure it is a number?')
- sys.exit(1)
-
- cur = self.get_cursor()
- book_infos = cur.execute(book_infos_req, (opts[0],)).fetchone()
- if book_infos is None:
- print('No such book {0}'.format(opts[0]))
- sys.exit(1)
- book_infos = dict(book_infos)
-
- cur.execute(
- "update t_item set f_title = ? where f_id_item = ?",
- (opts[1], book_infos['f_id_item']))
- print(cur.rowcount)
- print('Title successfully updated to {0}.'.format(opts[1]))
-
- def remote(self, opts=[]):
- if len(opts) == 1 and opts[1] == 'files':
- print('Here is your reader content:')
- file_listing_helper(
- os.path.join(self.reader_mount_point,
- self.config['remote_library_path']))
-
- else:
- self.device()
-
- def files(self, opts=[]):
- print('Here is your library content:')
- file_listing_helper(self.library_path)
-
- def device(self, opts=[]):
- import xml.etree.ElementTree as ET
- adobe_info = os.path.join(self.reader_mount_point,
- '.adobe-digital-editions',
- 'device.xml')
- adobe_sn = ''
- adobe_name = ''
- if os.path.isfile(adobe_info):
- tree = ET.parse(adobe_info)
- device_info = tree.find('./adept:deviceClass',
- {'adept': 'http://ns.adobe.com/adept'})
- if device_info is not None:
- adobe_name = device_info.text
- adobe_serial = tree.find('./adept:deviceSerial',
- {'adept': 'http://ns.adobe.com/adept'})
- if adobe_serial is not None:
- adobe_sn = adobe_serial.text
-
- device_name = bookeen_identify()
- if device_name is None:
- device_name = adobe_name
- print(device_name)
-
- cur = self.get_cursor()
- b_ver = dict(cur.execute('select f_desc from t_version order'
- ' by f_versiondate desc limit 1').fetchone())
- print('Internal software: {0}'.format(b_ver['f_desc']))
- p_id = read_file(os.path.join(
- self.reader_mount_point, 'system', 'product.dat'))
- print('Product ID: {0}'.format(p_id))
- p_sn = read_file(os.path.join(
- self.reader_mount_point, 'system', 'sn.txt'))
- print('Device serial: {0}'.format(p_sn))
- print('Adobe serial: {0}'.format(adobe_sn))
-
- def init(self, opts=[]):
- library_path = os.path.expanduser(opts[0])
- if not os.path.isdir(library_path):
- print('Please give us a valid path to your local library')
- sys.exit(1)
-
- config_path = os.path.join(library_path, '.eshelf.conf')
- confparser = ConfigParser()
-
- print('Please tell me where is your eReader mount point:')
- reader_mount_point = input('?> ')
-
- reader_db_path = os.path.join(reader_mount_point, 'system', 'library')
- if not os.path.isfile(reader_db_path):
- print('We cannot locate your eReader. '
- 'Please check you have plugged it.')
- sys.exit(1)
-
- confparser['DEFAULT']['reader_mount_point'] = reader_mount_point
-
- print('Default internal path of your eReader will be /mnt/fat. '
- 'You are free to change it now or just press return:')
- int_path = input('?> ')
- if int_path is not None and int_path != '':
- confparser['DEFAULT']['remote_physical_path'] = int_path
- else:
- confparser['DEFAULT']['remote_physical_path'] = '/mnt/fat'
-
- with open(config_path, 'w') as configfile:
- confparser.write(configfile)
-
-
-if __name__ == "__main__":
- predefined_actions = ['help', 'books', 'collections', 'add',
- 'remove', 'edit', 'init', 'files',
- 'remote', 'device']
- plugins_files = []
- plugins_path = os.path.join(xdg_config_home, 'eshelf', 'plugins')
- if os.path.isdir(plugins_path):
- if plugins_path not in sys.path:
- sys.path.append(plugins_path)
- plugins_files = [fname[:-3] for fname in os.listdir(plugins_path)
- if fname.endswith(".py")]
- predefined_actions += plugins_files
-
- parser = argparse.ArgumentParser(prog='eShelf')
- parser.add_argument('command', help='What to do',
- choices=predefined_actions)
- parser.add_argument('arguments', nargs='*',
- help='Supplementary options',
- default=[])
- parser.add_argument('--path', '-p', default=os.getcwd(),
- help='Path to your library.')
- args = parser.parse_args()
-
- if args.command == 'help':
- parser.print_help()
- sys.exit(0)
-
- library_path = args.path
- if args.command == 'init':
- library_path = None
-
- my_library = Shelf(library_path)
-
- if args.command in plugins_files:
- loc_plugin = __import__(args.command)
- the_library = loc_plugin.register(
- library_path, my_library.get_config())
- if len(args.arguments) == 0:
- loc_plugin.show_help()
- exit(2)
- the_command = args.arguments.pop(0)
-
- else:
- the_library = my_library
- the_command = args.command
-
- getattr(the_library, the_command)(args.arguments)
-
- the_library.get_reader_db().close()
diff --git a/eshelf/__init__.py b/eshelf/__init__.py
new file mode 100644
index 0000000..081f562
--- /dev/null
+++ b/eshelf/__init__.py
@@ -0,0 +1,2 @@
+__version__ = "0.1.0"
+__description__ = "eBook Reader management tool"
diff --git a/plugins/backup.py b/eshelf/backup.py
index 2750dbe..27e1baf 100644
--- a/plugins/backup.py
+++ b/eshelf/backup.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
import os
import shutil
from datetime import datetime
diff --git a/eshelf/bookeen.py b/eshelf/bookeen.py
new file mode 100644
index 0000000..2002359
--- /dev/null
+++ b/eshelf/bookeen.py
@@ -0,0 +1,40 @@
+import re
+import subprocess
+
+
+METADATA = {
+ 'muse': {
+ 'vendor_id': '0525',
+ 'product_id': 'a4a5',
+ 'bcd': '2.30',
+ 'formats': ['epub', 'html', 'fb2', 'txt', 'pdf', 'djvu'],
+ 'label': 'Bookeen Cybook Muse'
+ },
+ 'diva': {
+ 'vendor_id': '1d6b',
+ 'product_id': '0104',
+ 'bcd': '1',
+ 'formats': ['epub', 'html', 'fb2', 'txt', 'pdf'],
+ 'label': 'Bookeen Diva HD'
+ }
+}
+
+
+def identify():
+ for model, spec in METADATA.items():
+ prefix = ':'.join([spec['vendor_id'], spec['product_id']])
+ try:
+ details = subprocess.run(
+ ['lsusb', '-d', prefix, '-v'],
+ check=True, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ ).stdout.decode()
+ except subprocess.CalledProcessError:
+ continue
+ if details == '':
+ continue
+ m = re.search('bcdDevice\\s*([0-9.]+)', details)
+ if m is None or m.group(1) != spec['bcd']:
+ continue
+ return spec['label']
+ return None
diff --git a/eshelf/cli.py b/eshelf/cli.py
new file mode 100644
index 0000000..b7ebe07
--- /dev/null
+++ b/eshelf/cli.py
@@ -0,0 +1,234 @@
+import os
+import re
+import sys
+import argparse
+from configparser import ConfigParser
+from eshelf.setup import setup_local_library
+from eshelf.reader_orm import ReaderOrm
+from eshelf.utils import print_file_tree, print_listing, read_file
+from eshelf.bookeen import identify
+
+
+class EShelfCli:
+ def __init__(self):
+ self.reader_db = None
+ predefined_actions = ['help', 'books', 'collections', 'add',
+ 'remove', 'edit', 'setup', 'list',
+ 'device']
+
+ parser = argparse.ArgumentParser(prog='eshelf')
+ parser.add_argument('command', help='What to do',
+ choices=predefined_actions)
+ parser.add_argument('arguments', nargs='*',
+ help='Supplementary options',
+ default=[])
+ parser.add_argument('--path', '-p', default=os.getcwd(),
+ help='Path to your library.')
+ args = parser.parse_args()
+
+ if args.command == 'help':
+ parser.print_help()
+ self.exit()
+
+ if args.command == 'setup':
+ setup_local_library(args.path)
+ self.exit()
+
+ command = 'cmd_{}'.format(args.command)
+ if command not in dir(EShelfCli):
+ parser.print_help()
+ self.exit(2)
+
+ library_path = args.path
+
+ if not os.path.isdir(library_path):
+ print('{0} does not exists, please provide us a valid path.'
+ .format(library_path))
+ self.exit(1)
+
+ self.library_path = library_path.rstrip('/')
+ config_file = os.path.join(self.library_path, '.eshelf.conf')
+ if not os.path.isfile(config_file):
+ print('{0} is not a valid library folder. Please do first'
+ ' `eshelf setup`'.format(self.library_path))
+ self.exit(1)
+
+ confparser = ConfigParser()
+ confparser.read(config_file)
+ self.config = confparser['DEFAULT']
+
+ if 'reader_mount_point' not in self.config:
+ print('{0} has been corrupted. Please redo'
+ ' `eshelf setup`'.format(self.library_path))
+ self.exit(1)
+
+ self.reader_mount_point = self.config['reader_mount_point'].rstrip('/')
+ self.reader_db = ReaderOrm(
+ os.path.join(self.reader_mount_point, 'system', 'library')
+ )
+ if self.reader_db is None:
+ sys.exit(1)
+
+ self.remote_physical_path = self.config.get(
+ 'remote_physical_path', '/mnt/fat'
+ ).rstrip('/')
+
+ getattr(self, command)(args.arguments)
+ self.bye()
+
+ def bye(self, return_code=0):
+ if self.reader_db is not None:
+ self.reader_db.close()
+ sys.exit(return_code)
+
+ def cmd_collections(self, opts=[]):
+ finaldata = []
+ for data in self.reader_db.list_collections():
+ if data['f_readonly'] != 0:
+ continue
+ finaldata.append(
+ '[{0}] {1}'.format(data['f_id_shelf'], data['f_name'])
+ )
+ print_listing('Collections:', finaldata)
+
+ def cmd_books(self, opts=[]):
+ shelf_name = None
+ shelf_id = None
+ orphans = False
+ if len(opts) == 1:
+ if opts[0] == 'orphans':
+ orphans = True
+ else:
+ shelf_infos = self.reader_db.get_shelf(opts[0])
+ if shelf_infos is None:
+ return
+ shelf_name = shelf_infos['f_name']
+ shelf_id = shelf_infos['f_id_shelf']
+ finaldata = []
+ remote_library_path = os.path.join(
+ self.remote_physical_path,
+ self.config['remote_library_path']
+ )
+ for book_data in self.reader_db.list_shelf_items(shelf_id):
+ if not book_data['f_internal_uri'].startswith(remote_library_path):
+ # Maybe a system file? Or a default one?
+ continue
+
+ mounted_file_path = re.sub(
+ self.remote_physical_path,
+ self.reader_mount_point,
+ book_data['f_internal_uri']
+ )
+
+ relative_file_path = re.sub(
+ remote_library_path, '',
+ book_data['f_internal_uri']
+ ).lstrip('/')
+
+ lfile = os.path.join(self.library_path, relative_file_path)
+ local_file = ''
+ if orphans:
+ if os.path.isfile(lfile):
+ continue
+ local_file = ' ' + mounted_file_path
+ elif not os.path.isfile(lfile):
+ local_file = ' (only on ereader: {0})' \
+ .format(relative_file_path)
+ else:
+ local_file = ' ({0})'.format(relative_file_path)
+
+ if os.path.isfile(mounted_file_path):
+ finaldata.append('[{0}] {1}{2}'.format(
+ book_data['f_id_item'], book_data['f_title'], local_file))
+
+ if shelf_name is not None:
+ title = 'Books in {0}:'.format(shelf_name)
+ else:
+ title = 'Books in your eReader library:'
+ print_listing(title, finaldata)
+
+ def cmd_list(self, opts=[]):
+ if len(opts) == 1 and opts[0] == 'remote':
+ print('Here is your eReader content:')
+ print_file_tree(
+ os.path.join(self.reader_mount_point,
+ self.config['remote_library_path']))
+ else:
+ print('Here is your local library content:')
+ print_file_tree(self.library_path)
+
+ def prepare_add_remove(self, shelf_id_or_name, book_id_or_title):
+ shelf_infos = self.reader_db.get_shelf(shelf_id_or_name)
+ if shelf_infos is None:
+ self.bye(1)
+ book_infos = self.reader_db.get_book(book_id_or_title)
+ if book_infos is None:
+ self.bye(1)
+ return shelf_infos['f_id_shelf'], book_infos['f_id_item']
+
+ def cmd_add(self, opts=[]):
+ if len(opts) < 2:
+ print('Action must follow the form: add <id_or_title>'
+ ' <shelf_id_or_name>')
+ self.bye(1)
+ cur_shelf, cur_book = self.prepare_add_remove(opts[1], opts[0])
+ self.reader_db.add_shelf_link(cur_book, cur_shelf)
+ print('Successfully added a book to a shelf.')
+ self.books([cur_shelf])
+
+ def cmd_remove(self, opts=[]):
+ if len(opts) < 2:
+ print('Action must follow the form: remove <id_or_title>'
+ ' <shelf_id_or_name>')
+ self.bye(1)
+ cur_shelf, cur_book = self.prepare_add_remove(opts[1], opts[0])
+ self.reader_db.remove_shelf_link(cur_book, cur_shelf)
+ print('Successfully removed a book from a shelf.')
+ self.books([cur_shelf])
+
+ def cmd_edit(self, opts=[]):
+ if len(opts) < 2:
+ print('Action must follow the form: edit <id> <new_title>')
+ self.bye(1)
+ book_infos = self.reader_db.get_book(opts[0])
+ if book_infos is None:
+ self.bye(1)
+ self.reader_db.update_book_title(book_infos['f_id_item'], opts[1])
+ print('Title successfully updated to {0}.'.format(opts[1]))
+
+ def cmd_device(self, opts=[]):
+ import xml.etree.ElementTree as ET
+ adobe_info = os.path.join(self.reader_mount_point,
+ '.adobe-digital-editions',
+ 'device.xml')
+ adobe_sn = ''
+ adobe_name = ''
+ if os.path.isfile(adobe_info):
+ tree = ET.parse(adobe_info)
+ device_info = tree.find('./adept:deviceClass',
+ {'adept': 'http://ns.adobe.com/adept'})
+ if device_info is not None:
+ adobe_name = device_info.text
+ adobe_serial = tree.find('./adept:deviceSerial',
+ {'adept': 'http://ns.adobe.com/adept'})
+ if adobe_serial is not None:
+ adobe_sn = adobe_serial.text
+
+ device_name = identify()
+ if device_name is None:
+ device_name = adobe_name
+ print(device_name)
+
+ p_ver = self.reader_db.get_software_version()
+ print('Internal software: {0}'.format(p_ver))
+ p_id = read_file(os.path.join(
+ self.reader_mount_point, 'system', 'product.dat'))
+ print('Product ID: {0}'.format(p_id))
+ p_sn = read_file(os.path.join(
+ self.reader_mount_point, 'system', 'sn.txt'))
+ print('Device serial: {0}'.format(p_sn))
+ print('Adobe serial: {0}'.format(adobe_sn))
+
+
+if __name__ == '__main__':
+ EShelfCli()
diff --git a/eshelf/reader_orm.py b/eshelf/reader_orm.py
new file mode 100644
index 0000000..0cd462c
--- /dev/null
+++ b/eshelf/reader_orm.py
@@ -0,0 +1,106 @@
+import sqlite3
+
+
+class ReaderOrm:
+ def __init__(self, db_path):
+ self.reader_db = self.open_reader_db(db_path)
+
+ def open_reader_db(self, db_path):
+ try:
+ reader_db = sqlite3.connect(db_path)
+ reader_db.row_factory = sqlite3.Row
+ reader_db.isolation_level = None
+ except sqlite3.OperationalError:
+ print('Something wrong happen. Maybe you do not have'
+ ' mounted your ereader.')
+ return None
+ return reader_db
+
+ def close(self):
+ if self.reader_db is not None:
+ self.reader_db.close()
+
+ def get_cursor(self):
+ return self.reader_db.cursor()
+
+ def get_b_uuid(self):
+ b_uuid_req = 'select f_db_uuid from t_uuiddb limit 1'
+ b_uuid = dict(self.get_cursor().execute(b_uuid_req).fetchone())
+ return b_uuid['f_db_uuid']
+
+ def get_software_version(self):
+ b_ver_req = 'select f_desc from t_version ' \
+ 'order by f_versiondate desc limit 1'
+ b_ver = dict(self.get_cursor().execute(b_ver_req).fetchone())
+ return b_ver['f_desc']
+
+ def get_shelf(self, id_or_name):
+ if str(id_or_name).isdecimal():
+ shelf_infos_req = 'select f_id_shelf, f_name ' \
+ 'from t_shelf where f_id_shelf = ?'
+ else:
+ shelf_infos_req = 'select f_id_shelf, f_name ' \
+ 'from t_shelf where f_name = ? collate nocase'
+ cur = self.get_cursor()
+ shelf_infos = cur.execute(
+ shelf_infos_req, (id_or_name,)
+ ).fetchone()
+ if shelf_infos is None:
+ print('No such shelf {0}'.format(id_or_name))
+ return None
+ return dict(shelf_infos)
+
+ def get_book(self, id_or_title):
+ if str(id_or_title).isdecimal():
+ book_infos_req = 'select f_id_item, f_title ' \
+ 'from t_item where f_id_item = ?'
+ else:
+ book_infos_req = 'select f_id_item, f_title ' \
+ 'from t_item where f_title = ? collate nocase'
+ cur = self.get_cursor()
+ book_infos = cur.execute(
+ book_infos_req, (id_or_title,)
+ ).fetchone()
+ if book_infos is None:
+ print('No such book {0}'.format(id_or_title))
+ return None
+ return dict(book_infos)
+
+ def list_shelf_items(self, shelf_id=None):
+ cur = self.get_cursor()
+ if shelf_id is None:
+ query = 'select f_id_item, f_title, f_internal_uri ' \
+ 'from t_item order by f_title'
+ return [dict(row) for row in cur.execute(query)]
+ query = 'select b.f_id_item, b.f_title, b.f_internal_uri ' \
+ 'from t_item b, t_shelf_link tb where ' \
+ 'tb.f_item_id = b.f_id_item and tb.f_shelf_id = ? ' \
+ 'order by b.f_title'
+ return [dict(row) for row in cur.execute(query, (shelf_id,))]
+
+ def list_collections(self):
+ cur = self.get_cursor()
+ query = 'select f_id_shelf, f_name, f_readonly ' \
+ 'from t_shelf order by f_name'
+ return [dict(row) for row in cur.execute(query)]
+
+ def add_shelf_link(self, book_id, shelf_id):
+ b_uuid = self.reader_db.get_b_uuid()
+ self.get_cursor().execute(
+ 'insert into t_shelf_link values (?, ?, ?)',
+ (book_id, shelf_id, b_uuid)
+ )
+
+ def remove_shelf_link(self, book_id, shelf_id):
+ b_uuid = self.reader_db.get_b_uuid()
+ self.get_cursor().execute(
+ 'delete from t_shelf_link where '
+ 'f_item_id = ? and f_shelf_id = ? and f_db_uuid = ?',
+ (book_id, shelf_id, b_uuid)
+ )
+
+ def update_book_title(self, book_id, new_title):
+ self.get_cursor().execute(
+ 'update t_item set f_title = ? where f_id_item = ?',
+ (new_title, book_id)
+ )
diff --git a/eshelf/setup.py b/eshelf/setup.py
new file mode 100644
index 0000000..91cf7b2
--- /dev/null
+++ b/eshelf/setup.py
@@ -0,0 +1,47 @@
+import os
+import sys
+from configparser import ConfigParser
+
+
+def setup_local_library(library_path):
+ if library_path:
+ library_path = os.path.expanduser(library_path)
+ if not library_path or not os.path.isdir(library_path):
+ print('Please give us a valid path to your local library')
+ sys.exit(1)
+
+ config_path = os.path.join(library_path, '.eshelf.conf')
+ confparser = ConfigParser()
+
+ print('Please tell me where is your eReader mount point:')
+ reader_mount_point = input('?> ')
+
+ reader_db_path = os.path.join(reader_mount_point, 'system', 'library')
+ if not os.path.isfile(reader_db_path):
+ print('We cannot locate your eReader. '
+ 'Please check you have plugged it.')
+ sys.exit(1)
+
+ confparser['DEFAULT']['reader_mount_point'] = reader_mount_point
+
+ print('Default internal path of your eReader will be `/mnt/fat`. '
+ 'You are free to change it now or just press return:')
+ int_path = input('?> ')
+ if int_path is None or int_path == '':
+ int_path = '/mnt/fat'
+ confparser['DEFAULT']['remote_physical_path'] = int_path
+
+ print('Default remote path to your library will be `eBooks`. '
+ 'You are free to change it now or just press return:')
+ int_path = input('?> ')
+ if int_path is not None and int_path != '':
+ test_path = os.path.join(reader_mount_point, int_path)
+ if not os.path.isdir(test_path):
+ print('{} not found, fallback to default eBooks')
+ int_path = 'eBooks'
+ else:
+ int_path = 'eBooks'
+ confparser['DEFAULT']['remote_library_path'] = int_path
+
+ with open(config_path, 'w') as configfile:
+ confparser.write(configfile)
diff --git a/eshelf/utils.py b/eshelf/utils.py
new file mode 100644
index 0000000..82edbb7
--- /dev/null
+++ b/eshelf/utils.py
@@ -0,0 +1,45 @@
+import os
+
+
+def print_file_tree(current_dir, padding=None):
+ if padding is None:
+ padding = ''
+
+ decorator = '├──'
+ decorator_prefix = padding + '│ '
+
+ curfilelist = os.listdir(current_dir)
+ curfilelistlen = len(curfilelist) - 1
+ curpointer = 0
+ for curfile in curfilelist:
+ if curfile == '.eshelf.conf':
+ continue
+ if curpointer == curfilelistlen:
+ decorator = '└──'
+ decorator_prefix = padding + ' '
+ curpointer += 1
+ print('{0}{1} {2}'.format(padding, decorator, curfile))
+ curfile_path = os.path.join(current_dir, curfile)
+ if os.path.isdir(curfile_path):
+ print_file_tree(curfile_path, decorator_prefix)
+
+
+def print_listing(title, lines):
+ if len(lines) == 0:
+ return
+ print(title)
+ decorator = '├──'
+ curfilelistlen = len(lines) - 1
+ curpointer = 0
+ for line in lines:
+ if curpointer == curfilelistlen:
+ decorator = '└──'
+ curpointer += 1
+ print('{0} {1}'.format(decorator, line))
+
+
+def read_file(f):
+ if os.path.isfile(f):
+ with open(f, 'r') as s:
+ return s.read().strip()
+ return ''
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3e0960d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,24 @@
+[metadata]
+name = eshelf
+version = attr: eshelf.__version__
+description = attr: eshelf.__description__
+# long_description = file: README.md
+keywords = ebook, library, bookeen
+license = WTFPL
+author = Étienne Deparis
+author_email = etienne@depar.is
+url = https://git.deparis.io/eshelf/about/
+classifiers =
+ Development Status :: 3 - Alpha
+ Environment :: Console
+ Intended Audience :: End Users/Desktop
+ License :: What The F*** Public License (WTFPL)
+ Programming Language :: Python :: 3
+
+[options]
+zip_safe = True
+packages = find:
+
+[options.entry_points]
+console_scripts =
+ eshelf = eshelf.cli:EShelfCli
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..a4f49f9
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,2 @@
+import setuptools
+setuptools.setup()