Massive code refactoring, and some test rewrites.

This commit is contained in:
Mike Young
2019-11-15 14:45:42 -05:00
parent e012436011
commit d7279a73a7
127 changed files with 3960 additions and 345 deletions

View File

@@ -1,4 +0,0 @@
import os
import sys
sys.path.insert(0, os.path.abspath('.'))

View File

@@ -1,15 +0,0 @@
class Config:
"""Main System Configuration"""
def __init__(self):
self.book_path = "books/"
self.TITLE = "pyShelf E-Book Server"
self.VERSION = "0.1.0"
self.TITLE = self.TITLE + " ver" + self.VERSION
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

1
app/data/shelf.json Executable file

File diff suppressed because one or more lines are too long

0
app/lib/__init__.py Normal file → Executable file
View File

0
app/lib/api_hooks.py Normal file → Executable file
View File

31
app/lib/config.py Executable file
View File

@@ -0,0 +1,31 @@
import json
import os
import sys
class Config:
"""
Main System Configuration
"""
_fp = "config.json"
print(os.path)
def __init__(self, root=os.path.abspath('../')):
_data = self.open_file(root)
self.book_path = _data['BOOKPATH']
self.TITLE = _data['TITLE']
self.VERSION = _data['VERSION']
self.TITLE = self.TITLE + " ver " + self.VERSION
self.book_shelf = _data['BOOKSHELF']
# self.catalogue_db = "data/catalogue.db"
self.catalogue_db = root+'/'+_data['DATABASE']
self.file_array = [
self.book_shelf,
self.catalogue_db,
]
self.auto_scan = True
def open_file(self, root):
with open(root+'/'+self._fp, "r") as read_file:
data = json.load(read_file)
return data

View File

@@ -1,103 +0,0 @@
#!/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 = """
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta http-equiv=\"X-UA-Compatible\" content=\"IE-edge\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<link type=\"text/css\" rel=\"stylesheet\" href=\"/css/main.css\" />
<title>%s</title>
</head>
""" % self.TITLE
return _head
def app_Headers(self):
"""
App specific headers
:returns _head: HTML render of application specific headers
"""
_head = """
<body>
<div id=\"app\">
<div class=\"app_header\">
<h1 class=\"app_hdr shadow\">pyShelf</h1>
<h2> class=\"app_subhdr shadow\">Open Source E-book Server</h2>
</div>
"""
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 = """
<div class=\"app_body\">
<div class=\"left_col\">
%s
</div>
<div class=\"shelf\">
<div class=\"shelf_contents\">
%s
</div>
</div>
</div>
""" %(nav, shelf)
return _body
def app_footer(self):
"""
Main interface footer; Closes HTML
:returns _footer: HTML render of page footer
"""
_footer = """
<div class=\"app_footer\">
<div class=\"python_logo\">
<img src=\"/img/py.png\" id=\"python_logo\" />
</div>
</div>
</div>
</body>
</html>
"""
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

View File

@@ -7,39 +7,42 @@ 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
from .api_hooks import DuckDuckGo
from .config import Config
from .storage import Storage
config = Config()
# config = Config()
class Catalogue:
"""Decodes and stores book information"""
"""Step One: filter_books"""
def __init__(self):
def __init__(self, root):
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')
self.root_dir = root
_config = Config(root)
self.book_folder = _config.book_path
self.book_shelf = _config.book_shelf
self._book_list_expanded = None
self.books = None
def scan_folder(self, folder=config.book_path):
def scan_folder(self, _path=None):
if _path is not None:
folder = _path
elif os.path.isdir(self.root_dir+'/'+self.book_folder):
folder = self.root_dir+'/'+self.book_folder
else: folder = self.book_folder
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
@@ -50,17 +53,20 @@ class Catalogue:
"""
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:
try:
self.books = list(filter(regx.search, filter(None, self.file_list)))
except TypeError as e:
print(e)
self._book_list_expanded = {}
with open(self.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
self._book_list_expanded[book] = self.process_book(book)
json.dump(self._book_list_expanded, f)
return self._book_list_expanded
def process_book(self, book):
@staticmethod
def process_book(book):
"""Return dictionary of epub file contents"""
f_name = 'content.opf'
book = zipfile.ZipFile(book, 'r')
details = {}
with book as book_zip:
@@ -86,11 +92,11 @@ class Catalogue:
content = self.extract_content(book_zip, book)
soup = BeautifulSoup(content, "lxml")
title = soup.find("dc:title")
if title == None:
if title is 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]
if author is not None: author = author.contents[0]
try: cover = self.extract_cover_image(book_zip, book)
except IndexError:
# cover = self.extract_cover_html(book_zip, book)
@@ -127,8 +133,8 @@ class Catalogue:
db = Storage()
stored = db.book_paths_list()
closed = db.close()
try: self.books
except Exception: self.filter_books()
if self.books is None:
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])

View File

@@ -1,15 +1,11 @@
#!/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
from .config import Config
from .storage import Storage
config = Config()
Storage = Storage()
# config = Config()
# Storage = Storage()
class InitFiles:
@@ -29,47 +25,10 @@ class InitFiles:
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):
def __init__(self, **kwargs):
"""
Initialize class variables
:return: None
@@ -79,6 +38,8 @@ class BookDisplay:
self.thumbnail_size = [200, 300]
self.thumbnail_scale = 1
self.total_pages = None
try: self.screen_size = kwargs['screen_size']
except Exception: self.screen_size = [900, 600]
def nextPage(self):
"""
@@ -105,45 +66,4 @@ class BookDisplay:
"""
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
self.books_per_page = int(self.screen_size[0]//x) * int(self.screen_size[1]//y)

14
app/lib/storage.py Normal file → Executable file
View File

@@ -3,18 +3,20 @@ import sqlite3
import sys
# sys.path.insert(1, '../')
from config import Config
from .config import Config
db_pointer = Config().catalogue_db
# db_pointer = Config().catalogue_db
class Storage:
class Storage():
"""Contains all methods for system storage"""
def __init__(self):
def __init__(self, db_pointer=None):
# Optionaly pass db_file to specify another db or for testing
if db_pointer is None: db_pointer = Config().catalogue_db
self.db_file = db_pointer
self.database()
self.create_tables()
# self.create_tables()
def database(self):
"""Create database cursor"""
@@ -23,6 +25,8 @@ class Storage:
self.cursor = self.db.cursor()
return True
except Exception as e:
print(self.db_file)
print(e)
return False
def create_tables(self):

View File

@@ -1,21 +1,14 @@
#!/usr/bin/python
import os
import sys
from config import Config
from lib.display import Frontend
from lib.config import Config
from lib.library import Catalogue
from lib.pyShelf import BookDisplay, BookServer, InitFiles
from lib.pyShelf import InitFiles
# sys.path.insert(1, 'lib/')
config = Config() # Get configuration settings
ROOT_DIR = os.path.abspath('../')
sys.path.append(ROOT_DIR)
config = Config(ROOT_DIR) # 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 = Catalogue(ROOT_DIR) # Open the Catalogue
Catalogue.import_books() # Filter Your books
# Server.run()
# TODO Figure out a system to get books page count
# TODO Update Documentation
# TODO Requirements.txt

0
app/pyproject.toml Normal file → Executable file
View File

0
app/static/css/main.css Normal file → Executable file
View File

0
app/static/index.html Normal file → Executable file
View File

0
app/tests/__init__.py Executable file
View File

16
app/tests/config_test.py Executable file
View File

@@ -0,0 +1,16 @@
import os
from ..lib.config import Config
class TestConfig:
config = Config(os.path.abspath(os.path.curdir))
def test_book_dir(self):
assert os.path.isdir(self.config.book_path)
def test_title(self):
assert 'pyShelf' in self.config.TITLE
def test_version(self):
assert self.config.VERSION is not None

35
app/tests/library_test.py Executable file
View File

@@ -0,0 +1,35 @@
import json
import os
from ..lib.config import Config
from ..lib.library import Catalogue
class Test_Config(Config):
def __init__(self):
Config.__init__(self, 'config.json')
_data = self.open_file()
def open_file(self, root='config.json'):
with open('config.json') as read_file:
data = json.load(read_file)
return data
class Test_Catalogue(Catalogue):
def __init__(self):
Catalogue.__init__(self, root=os.path.abspath('.'))
def filter_books(self):
self.book_shelf = 'app/'+self.book_shelf
return super().filter_books()
class TestCatalogue:
root = os.path.abspath(os.path.curdir)
config = Test_Config()
def test_filter_books(self):
book_list = Test_Catalogue().filter_books()
assert len(book_list) > 0