diff --git a/app/__init__.py b/app/__init__.py
new file mode 100755
index 0000000..9a3cbc8
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,4 @@
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('.'))
diff --git a/app/config.py b/app/config.py
new file mode 100755
index 0000000..b4200e7
--- /dev/null
+++ b/app/config.py
@@ -0,0 +1,13 @@
+class Config:
+ """Main System Configuration"""
+ def __init__(self):
+ self.book_path = "books/"
+ self.TITLE = "pyShelf E-Book Server"
+ self.book_shelf = "data/shelf.json"
+ # self.catalogue_db = "data/catalogue.db"
+ self.catalogue_db = "../frontend/db.sqlite3"
+ self.file_array = [
+ self.book_shelf,
+ self.catalogue_db,
+ ]
+ self.auto_scan = True
diff --git a/app/lib/__init__.py b/app/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/lib/api_hooks.py b/app/lib/api_hooks.py
new file mode 100644
index 0000000..239f88a
--- /dev/null
+++ b/app/lib/api_hooks.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+import sys
+
+import requests
+
+# sys.path.insert(1, 'lib/')
+
+
+class DuckDuckGo:
+ """duckduckgo related searching"""
+ def __init__(self):
+ self.url = "https://api.duckduckgo.com/?q="
+
+ def image_result(self, query):
+ """
+ Returns json containing url to image
+ :param _key: &t=h_&iar=images&iax=images&ia=images&format=json&pretty=1
+ """
+ _key = '&t=h_&iar=images&iax=images&ia=images&format=json&pretty=1'
+ try: query = query.string
+ except AttributeError: query = query
+ search_result = requests.get(self.url+query+_key)
+ try: image_result = search_result.json()['Image']
+ except ValueError:
+ image_result = ''
+ if search_result.status_code == 200 and image_result != '':
+ image = requests.get(search_result.json()['Image'], stream=True)
+ image.raw.decode_content = True
+ return image.raw
+ else: return False
diff --git a/app/lib/display.py b/app/lib/display.py
new file mode 100644
index 0000000..9e89641
--- /dev/null
+++ b/app/lib/display.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+import cgi
+import sys
+
+from config import Config
+
+sys.path.insert(0, '../')
+
+
+class Frontend():
+ """Dynamic frontend display functions"""
+
+ def __init__(self, dimensions=[0, 0]):
+ """
+ :param dimensions: array containing screen size [x, y]
+ """
+ self.dimensions = dimensions
+ self.TITLE = Config().TITLE
+
+ def html_Headers(self):
+ """
+ HTML headers
+ :returns _head: HTML render of page headers
+ """
+ _head = """
+
+
+
+
+
+
+
+ %s
+
+ """ % self.TITLE
+ return _head
+
+ def app_Headers(self):
+ """
+ App specific headers
+ :returns _head: HTML render of application specific headers
+ """
+ _head = """
+
+
+
+ """
+ return _head
+
+ def app_body(self, nav, shelf):
+ """
+ Main interface body, and navigation
+ :param nav: nav[] system navigation list
+ :param shelf: shelf[0{path:"",title:"",cover:"",author:""}]
+ :returns _body: HTML render of page body
+ """
+ _body = """
+
+ """ %(nav, shelf)
+ return _body
+
+ def app_footer(self):
+ """
+ Main interface footer; Closes HTML
+ :returns _footer: HTML render of page footer
+ """
+ _footer = """
+
+
+
+
+ """
+ return _footer
+
+ def compile(self, nav, shelf):
+ """
+ Compiles user interface
+ :returns _ui: Compiled HTML for page layout
+ """
+ _head = self.html_Headers() + self.app_Headers()
+ _body = self.app_body(nav, shelf)
+ _foot = self.app_footer()
+ try:
+ _ui = _head + _body + _foot
+ return _ui
+ except Exception as e:
+ return e
diff --git a/app/lib/library.py b/app/lib/library.py
new file mode 100755
index 0000000..dde8525
--- /dev/null
+++ b/app/lib/library.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+import json
+import os
+import re
+import zipfile
+
+from bs4 import BeautifulSoup
+from PIL import Image
+
+from config import Config
+from lib.api_hooks import DuckDuckGo
+from lib.storage import Storage
+
+config = Config()
+
+
+class Catalogue:
+ """Decodes and stores book information"""
+ """Step One: filter_books"""
+
+ def __init__(self):
+ self.file_list = []
+ self.opf_regx = re.compile(r'\.opf')
+ self.cover_regx = re.compile(r'\.jpg|\.jpeg|\.png|\.bmp|\.gif')
+ self.html_regx = re.compile(r'\.html')
+
+ def scan_folder(self, folder=config.book_path):
+ for f in os.listdir(folder):
+ _path = os.path.abspath(folder+'/'+f)
+ #_path = os.path.abspath('.')+'/'+folder+f+'/'
+ _is_dir = os.path.isdir(_path.strip()+'/')
+ if _is_dir:
+ self.file_list.append(self.scan_folder(_path))
+ self.file_list.append(_path)
+
+ def scan_book(self, book):
+ """REMOVE ME?"""
+ _epub = zipfile.ZipFile(book)
+ with _epub as _epub_open:
+ try: _epub_open.open('content.opf'); return True
+ except Exception as e: print(e); return False
+
+ def filter_books(self):
+ """
+ Scan book folder recursively for epub files
+ filter_books(0) -> Catalogue.books
+ filter_books(1) -> self.books[]
+ :param ret: 0 -> create class property -> dump json
+ :param ret: 1 -> create & return class property
+ """
+ self.scan_folder()
+ regx = re.compile(r"\.epub")
+ self.books = list(filter(regx.search, filter(None, self.file_list)))
+ _book_list_expanded = {}
+ with open(config.book_shelf, 'w') as f:
+ for book in self.books:
+ _book_list_expanded[book] = self.process_book(book)
+ json.dump(_book_list_expanded, f)
+ return _book_list_expanded
+
+ def process_book(self, book):
+ """Return dictionary of epub file contents"""
+ f_name = 'content.opf'
+ book = zipfile.ZipFile(book, 'r')
+ details = {}
+ with book as book_zip:
+ details['files'] = []
+ details['path'] = book.filename
+ expanded = book_zip.infolist()
+ regx = re.compile(r'\.opf|cover')
+ for i in expanded:
+ match = re.search(regx, i.filename)
+ if match:
+ # Returns zip file location of requested files
+ details['files'].append(match.string)
+ return details
+
+ def extract_metadata(self, book):
+ """
+ Return extracted metadata and cover picture
+ book['path'] == Full path to ebook file
+ book['files'] == list of files from self.process_book(book)
+ """
+ book_zip = zipfile.ZipFile(book['path'], 'r')
+ with book_zip as f:
+ content = self.extract_content(book_zip, book)
+ soup = BeautifulSoup(content, "lxml")
+ title = soup.find("dc:title")
+ if title == None:
+ title = book['path'].split('/')[-1].rsplit('.', 1)[0]
+ else: title = title.contents[0]
+ author = soup.find("dc:creator")
+ if author != None: author = author.contents[0]
+ try: cover = self.extract_cover_image(book_zip, book)
+ except IndexError:
+ # cover = self.extract_cover_html(book_zip, book)
+ cover = DuckDuckGo().image_result(title)
+ book_details = [title, author, cover, book['path']]
+ return book_details
+
+ def extract_content(self, book_zip, book):
+ content = book_zip.open(
+ list(
+ filter(self.opf_regx.search, book['files'])
+ )[0]
+ )
+ return content
+
+ def extract_cover_html(self, book_zip, book):
+ cover = book_zip.open(
+ list(
+ filter(self.html_regx.search, book['files'])
+ )[0]
+ )
+ return cover
+
+ def extract_cover_image(self, book_zip, book):
+ cover = book_zip.open(
+ list(
+ filter(self.cover_regx.search, book['files'])
+ )[0]
+ )
+ try: cover = book_zip.read(cover.name); return cover
+ except KeyError: return False
+
+ def compare_shelf_current(self):
+ db = Storage()
+ stored = db.book_paths_list()
+ closed = db.close()
+ try: self.books
+ except Exception: self.filter_books()
+ on_disk, in_storage = [], []
+ for _x in self.books: on_disk.append(_x)
+ for _y in stored: in_storage.append(_y[0])
+ a, b, = set(on_disk), set(in_storage)
+ c = set.difference(a, b)
+ return c
+
+ def import_books(self, list=None):
+ book_list = self.compare_shelf_current()
+ db = Storage()
+ for book in book_list:
+ book = self.process_book(book)
+ extracted = self.extract_metadata(book)
+ db.insert_book(extracted)
+ inserted = db.commit()
+ if inserted is not True:
+ print(inserted)
+ if input('Continue ? y/n') == 'y':
+ pass
+ db.close()
diff --git a/app/lib/pyShelf.py b/app/lib/pyShelf.py
new file mode 100755
index 0000000..25f42d6
--- /dev/null
+++ b/app/lib/pyShelf.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+import mimetypes
+import os
+import zipfile
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+from config import Config
+from lib.library import Catalogue
+from lib.storage import Storage
+
+config = Config()
+Storage = Storage()
+
+
+class InitFiles:
+ """First run file creation operations"""
+ def __init__(self, file_array):
+ print("Begining creation of file structure")
+ for _pointer in file_array:
+ if not os.path.isfile(_pointer):
+ self.CreateFile(_pointer)
+ print("Concluded file creation")
+
+ def CreateFile(self, _pointer):
+ """Create the file"""
+ if not os.path.isdir(os.path.split(_pointer)[0]):
+ os.mkdir(os.path.split(_pointer)[0])
+ f = open(_pointer, "w+")
+ f.close()
+
+
+class RequestHandler(BaseHTTPRequestHandler):
+ """Request Handler"""
+ def do_GET(self):
+ # TODO determine how to include stylesheets
+ self.send_response(200)
+ if self.path == '/':
+ self.path = '../static/index.html'
+ mimetype = 'text/html'
+ serve_file = open(self.path[1:]).read()
+ self.send_header('Content-type', mimetype)
+ self.end_headers()
+ self.wfile.write(bytes(serve_file, 'utf-8'))
+ elif self.path.split('.', 1)[1] == 'css':
+ self.path = '../static' + self.path
+ mimetype = 'text/css'
+ serve_file = open(self.path[1:]).read()
+ self.send_header('Content-type', mimetype)
+ self.end_headers()
+ self.wfile.write(bytes(serve_file, 'utf-8'))
+ elif self.path.endswith('.png'):
+ self.path = '../static' + self.path
+ mimetype = 'image/png'
+ serve_file = open(self.path[1:], 'rb')
+ # Important to rb read binary for images
+ self.send_header('Content-type', mimetype)
+ self.end_headers()
+ self.wfile.write(serve_file.read())
+ else:
+ self.send_response(404)
+ serve_file = "File Not Found"
+ mimetype = 'text/html'
+ self.send_header('Content-type', mimetype)
+ self.end_headers()
+ try: serve_file.close()
+ except Exception: pass
+
+
+class BookDisplay:
+ """All functions related to displaying book information in the HTML UI"""
+
+ def __init__(self):
+ """
+ Initialize class variables
+ :return: None
+ """
+ self.books_per_page = None
+ self.current_page = 0
+ self.thumbnail_size = [200, 300]
+ self.thumbnail_scale = 1
+ self.total_pages = None
+
+ def nextPage(self):
+ """
+ Goto next book page
+ :return: new current_page
+ """
+ self.current_page += 1
+ return self.current_page
+
+ def previousPage(self):
+ """
+ Goto previous book page
+ :return: new current_page
+ """
+ self.current_page -= 1
+ return self.current_page
+
+ def booksPerPage(self, screen_size):
+ """
+ Set books per page
+
+ :param screen_size: Array containing x,y pixel sizes
+ :return: self.books_per_page
+ """
+ x = (self.thumbnail_size[0] * self.thumbnail_scale) + 10
+ y = (self.thumbnail_size[1] * self.thumbnail_scale) + 10
+ self.books_per_page = int(screen_size[0]//x) * int(screen_size[1]//y)
+ return self.books_per_page
+
+
+class BookServer:
+ """
+ HTTP server functions required to display e-books
+ """
+
+ def __init__(self):
+ self.server_address = ('', 8000)
+ self.handler = RequestHandler
+ self.httpd = HTTPServer(self.server_address, self.handler)
+
+ def close_prompt(self):
+ """Prompt to close server"""
+ close = input("Close Server? y/n")
+ if close == 'y':
+ self.close()
+ return True
+ else:
+ self.close_prompt()
+
+ def run(self):
+ """Start HTTP Server"""
+ try:
+ print("Server running @ http://127.0.0.1:8000")
+ self.httpd.serve_forever()
+ self.httpd.handle_request()
+ except KeyboardInterrupt:
+ print("Interrupt received, Closing Server")
+ self.close()
+ print("Server shutdown, Goodbye!")
+ return True
+
+ def close(self):
+ """Stop HTTP Server"""
+ try:
+ self.httpd.server_close()
+ return True
+ except Exception:
+ return False
diff --git a/app/lib/storage.py b/app/lib/storage.py
new file mode 100644
index 0000000..196cc95
--- /dev/null
+++ b/app/lib/storage.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+import sqlite3
+import sys
+
+# sys.path.insert(1, '../')
+from config import Config
+
+db_pointer = Config().catalogue_db
+
+
+class Storage:
+ """Contains all methods for system storage"""
+
+ def __init__(self):
+ self.db_file = db_pointer
+ self.database()
+ self.create_tables()
+
+ def database(self):
+ """Create database cursor"""
+ try:
+ self.db = sqlite3.connect(self.db_file)
+ self.cursor = self.db.cursor()
+ return True
+ except Exception as e:
+ return False
+
+ def create_tables(self):
+ """Create table structure"""
+ q_check = "SELECT * FROM books"
+ q_create = '''CREATE TABLE books(title text, author text,
+ categories text, cover blob, pages int, progress int,
+ file_name text)'''
+ try:
+ self.cursor.execute(q_check)
+ except sqlite3.OperationalError as e:
+ self.cursor.execute(q_create)
+
+ def insert_book(self, book):
+ """
+ Insert book in database
+ :returns: True if succeeds False if not
+ """
+ q_x = '''SELECT title FROM books WHERE EXISTS(SELECT * from books WHERE `title` = ?)'''
+ q = '''INSERT INTO books (title, author, cover, file_name) values (?, ?, ?, ?)'''
+ try:
+ try: cover_image = book[2].data
+ except: cover_image = book[2]
+ x = self.cursor.execute(q_x, (book[0],))
+ try: len(x.fetchone()) > 0
+ except Exception:
+ if not book[2]: # If cover image is missing unset entry
+ cover_image = None
+ self.cursor.execute(q, (book[0], book[1], cover_image, book[3]))
+ return True
+ except Exception as e:
+ print(e)
+ return False
+
+ def book_paths_list(self):
+ q = '''SELECT file_name FROM books'''
+ x = self.cursor.execute(q)
+ try: x = x.fetchall()
+ except Exception: x = []
+ return x
+
+ def commit(self):
+ try: self.db.commit(); return True
+ except Exception as e: return e
+
+ def close(self):
+ self.db.close()
+ return True
diff --git a/app/main.py b/app/main.py
new file mode 100755
index 0000000..8011fb7
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,21 @@
+#!/usr/bin/python
+import sys
+
+from config import Config
+from lib.display import Frontend
+from lib.library import Catalogue
+from lib.pyShelf import BookDisplay, BookServer, InitFiles
+
+# sys.path.insert(1, 'lib/')
+
+config = Config() # Get configuration settings
+InitFiles(config.file_array) # Initialize file system
+Catalogue = Catalogue() # Open the Catalogue
+UI = Frontend()
+Server = BookServer()
+# new_books = Catalogue.new_files()
+Catalogue.import_books() # Filter Your books
+# Server.run()
+# TODO Figure out a system to get books page count
+# TODO Update Documentation
+# TODO Requirements.txt
diff --git a/app/pyproject.toml b/app/pyproject.toml
new file mode 100644
index 0000000..31ec3a3
--- /dev/null
+++ b/app/pyproject.toml
@@ -0,0 +1,10 @@
+[tool.isort]
+force_grid_wrap = 0
+include_trailing_comma = true
+line_length = 88
+multi_line_output = 3
+use_parentheses = true
+# NOTE: the known_third_party setting is managed by
+# seed-isort-config and should not be modified directly.
+# Any changes made to this setting will be overwritten.
+known_third_party = ["PIL", "bs4", "requests"]
diff --git a/app/static/css/main.css b/app/static/css/main.css
new file mode 100644
index 0000000..e3371a8
--- /dev/null
+++ b/app/static/css/main.css
@@ -0,0 +1,69 @@
+body{
+ margin: 0px 10px 0px 10px;
+ padding: 0px;
+ background-color: #DCDCDD;
+ color: #46494C
+}
+#app{
+ display: grid;
+ grid-template-areas: "app_header"
+ "app_body"
+ "app_footer";
+ grid-template-rows: 5vh 90vh 5vh;
+ max-height: 100%
+}
+.app_header{
+ grid-area: app_header;
+ display: grid;
+ grid-template-areas: "title slogan";
+ align-items: center;
+}
+.app_hdr{
+ grid-area: title;
+ margin: 0px;
+ font-family: 'Audiowide', cursive;
+ font-size: 25px;
+ text-align: start;
+}
+.shadow{
+ text-shadow: #4c5c68 -5px 3px 5px;
+}
+.app_subhdr{
+ grid-area: slogan;
+ margin: 0px;
+ font-family: 'Audiowide', cursive;
+ font-size: 18px;
+ text-shadow: #4c5c68 -5px 3px 5px;
+ text-align: end;
+}
+.app_body{
+ display: grid;
+ grid-area: app_body;
+ grid-template-columns: 20% 80%;
+ grid-template-areas: "left_col shelf";
+}
+.app_footer{
+ grid-area: app_footer;
+}
+.left_col{
+ grid-area: left_col
+}
+.shelf{
+ grid-area: shelf;
+ margin: 0px auto 0px auto;
+}
+.shelf_contents{
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+.right_col{
+ grid-area: right_col
+}
+.python_logo{
+
+}
+#python_logo{
+ height: 50px;
+ width: 91px;
+}
diff --git a/app/static/img/py.png b/app/static/img/py.png
new file mode 100755
index 0000000..828eac7
Binary files /dev/null and b/app/static/img/py.png differ
diff --git a/app/static/img/shelf.png b/app/static/img/shelf.png
new file mode 100755
index 0000000..fdffe48
Binary files /dev/null and b/app/static/img/shelf.png differ
diff --git a/app/static/index.html b/app/static/index.html
new file mode 100644
index 0000000..9e95ecb
--- /dev/null
+++ b/app/static/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+pyShelf E-Book Server
+
+
+
+
+
+