diff --git a/config.json b/config.json index 71951be..28132cf 100755 --- a/config.json +++ b/config.json @@ -1 +1 @@ -{"TITLE": "pyShelf E-Book Server", "VERSION": "0.5.0", "BOOKPATH": "", "DB_HOST": "localhost", "DB_PORT": "5432", "DATABASE": "pyshelf", "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", "ALLOWED_HOSTS": "*", "hostname": "localhost", "webport": "8000", "wsgiport": "8001"} +{"TITLE": "pyShelf E-Book Server", "VERSION": "0.5.0", "BOOKPATH": "/home/raelon/Books", "DB_HOST": "localhost", "DB_PORT": "5432", "DATABASE": "pyshelf", "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", "ALLOWED_HOSTS": "*", "hostname": "localhost", "webport": "8000", "wsgiport": "8001"} diff --git a/importBooks b/importBooks index 244906d..272b643 100755 --- a/importBooks +++ b/importBooks @@ -1,4 +1,4 @@ -#!python +#!/usr/bin/env python import pathlib import sys diff --git a/importBooks.pstat b/importBooks.pstat new file mode 100644 index 0000000..e69de29 diff --git a/installer b/installer index f6882b5..b8c112b 100755 --- a/installer +++ b/installer @@ -1,4 +1,4 @@ -#!python +#!/usr/bin/ env python import json import os import pathlib diff --git a/makeCollections b/makeCollections index 4333055..0e82de7 100755 --- a/makeCollections +++ b/makeCollections @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import pathlib import sys diff --git a/preview_060_newui_collections_wip.png b/preview_060_newui_collections_wip.png new file mode 100644 index 0000000..ccefeeb Binary files /dev/null and b/preview_060_newui_collections_wip.png differ diff --git a/preview_060_newui_wip.png b/preview_060_newui_wip.png new file mode 100644 index 0000000..ba2b7a8 Binary files /dev/null and b/preview_060_newui_wip.png differ diff --git a/pyproject.toml b/pyproject.toml index eaa5fde..6caf254 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,4 +7,4 @@ 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 = ["backend", "bs4", "django", "interface", "prompt_toolkit", "psycopg2", "pyfiglet", "requests"] +known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2", "pyfiglet", "requests"] diff --git a/requirements.txt b/requirements.txt index 749a3dd..89974a9 100755 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ psycopg2-binary prompt_toolkit psutil pyfiglet +mobi-python diff --git a/src/backend/lib/library.py b/src/backend/lib/library.py index 6585f6d..1292a5d 100755 --- a/src/backend/lib/library.py +++ b/src/backend/lib/library.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import json import os import pathlib @@ -7,6 +7,8 @@ import zipfile from bs4 import BeautifulSoup +from mobi import Mobi + from .api_hooks import DuckDuckGo from .config import Config from .storage import Storage @@ -26,8 +28,6 @@ class Catalogue: self.html_regx = re.compile(r"\.html") self.root_dir = config.root self.book_folder = config.book_path - self.book_shelf = config.book_shelf - self._book_list_expanded = None self.books = None self.db_pointer = config.catalogue_db self.config = config @@ -56,24 +56,30 @@ class Catalogue: :returns self._book_list_expanded: json string containing all book metadata """ - self.scan_folder() # Populate file list - regx = re.compile(r"\.epub") + self.scan_folder() # Populate file list + regx = re.compile(r"\.epub|\.mobi") 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: - self._book_list_expanded[book] = self.process_book(book) - json.dump(self._book_list_expanded, f) + """ + for book in self.books: + self._book_list_expanded[book] = self.process_by_filetype(book) return self._book_list_expanded + """ + + def process_by_filetype(self, book): + if book.endswith(".epub"): + epub = self.process_epub(book) + return self.extract_metadata_epub(epub) + elif book.endswith(".mobi"): + return self.extract_metadata_mobi(book) @staticmethod - def process_book(book): + def process_epub(book): """Return dictionary of epub file contents""" - book = zipfile.ZipFile(book, "r") details = {} + book = zipfile.ZipFile(book, "r") with book as book_zip: details["files"] = [] details["path"] = book.filename @@ -86,7 +92,7 @@ class Catalogue: details["files"].append(match.string) return details - def extract_metadata(self, book): + def extract_metadata_epub(self, book): """ Return extracted metadata and cover picture book['path'] == Full path to ebook file @@ -94,7 +100,7 @@ class Catalogue: """ book_zip = zipfile.ZipFile(book["path"], "r") with book_zip as f: - content = self.extract_content(book_zip, book) + content = self.extract_content(f, book) soup = BeautifulSoup(content, "lxml") title = soup.find("dc:title") if title is None: @@ -105,13 +111,89 @@ class Catalogue: if author is not None: author = author.contents[0] try: - cover = self.extract_cover_image(book_zip, book) + cover = self.extract_cover_image(f, book) except IndexError: # cover = self.extract_cover_html(book_zip, book) cover = DuckDuckGo().image_result(title) - book_details = [title, author, cover, book["path"]] + try: + description = self.stripTags(soup.find("dc:description").text) + except AttributeError: + description = None + try: + identifier = self.stripTags(soup.find("dc:identifier").text) + except AttributeError: + identifier = None + try: + publisher = self.stripTags(soup.find("dc:publisher").text) + except AttributeError: + publisher = None + try: + date = self.stripTags(soup.find("dc:date").text) + except AttributeError: + date = None + try: + rights = self.stripTags(soup.find("dc:rights").text) + except AttributeError: + rights = None + try: + tags = soup.find_all("dc:subject") + except AttributeError: + tags = None + ftags = None + if tags is not None: + for tag in tags: + if ftags is None: + ftags = tag.text + else: + ftags = ftags + "," + tag.text + book_details = [ + title, + author, + cover, + book["path"], + description, + identifier, + publisher, + date, + rights, + ftags, + ] return book_details + @staticmethod + def stripTags(source): + p = re.compile(r"<.*?>") + return p.sub("", source) + + @staticmethod + def extract_metadata_mobi(book): + book = Mobi(book) + book.parse() + try: + cover_image = book.readImageRecord(0) + except KeyError: + cover_image = None + title = book.title().decode("utf-8") + author = book.author().decode("utf-8") + description = None + identifier = None + publisher = None + date = None + rights = None + ftags = None + return [ + title, + author, + cover_image, + book.f.name, + description, + identifier, + publisher, + date, + rights, + ftags, + ] + def extract_content(self, book_zip, book): """ Opens epub as zip file filters then stores as list any files matching opf_regx @@ -161,12 +243,13 @@ class Catalogue: Gets a list of new files via compare_shelf_current. Iterates over list and inserts new books into database. """ + # TODO Refactor metadata extraction into process_book \ + # call to more easily handle additional formats book_list = self.compare_shelf_current() db = Storage(self.config) for book in book_list: - book = self.process_book(book) - extracted = self.extract_metadata(book) - db.insert_book(extracted) + book = self.process_by_filetype(book) + db.insert_book(book) inserted = db.commit() if inserted is not True: print(inserted) diff --git a/src/backend/lib/storage.py b/src/backend/lib/storage.py index 953d8ec..1e33ff2 100755 --- a/src/backend/lib/storage.py +++ b/src/backend/lib/storage.py @@ -53,7 +53,7 @@ class Storage: Insert book in database :returns: True if succeeds False if not """ - q = "INSERT INTO books (title, author, cover, progress, file_name, pages) values (%s, %s, %s, 0, %s, 0);" + q = "INSERT INTO books (title, author, cover, progress, file_name, pages, description, identifier, publisher, date, rights, tags) values (%s, %s, %s, 0, %s, 0, %s, %s, %s, %s, %s, %s);" try: try: cover_image = book[2].data @@ -61,9 +61,26 @@ class Storage: cover_image = book[2] 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])) + self.cursor.execute( + q, + ( + book[0], # title + book[1], # author + cover_image, + book[3], # file + book[4], # descr + book[5], # ident + book[6], # publisher + book[7], # date + book[8], # rights + book[9], # tags + ), + ) return True except Exception as e: + if int(e.pgcode) == 22007: + book[7] = psycopg2.Date(int(book[7]), 1, 1) + self.insert_book(book) print(e) return False diff --git a/src/backend/pyShelf_MakeCollections.py b/src/backend/pyShelf_MakeCollections.py index 0f16549..139e394 100755 --- a/src/backend/pyShelf_MakeCollections.py +++ b/src/backend/pyShelf_MakeCollections.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import os import sys import time @@ -16,7 +16,7 @@ def MakeCollections(root): config = Config(root) # Get configuration settings # InitFiles(config.file_array) # Initialize file system _storage = Storage(config) - _storage.make_collections() + _storage.make_collections() _t2 = time.time() scan_time = round(_t2 - _t1) print("Collections Made.") diff --git a/src/backend/pyShelf_ScanLibrary.py b/src/backend/pyShelf_ScanLibrary.py index 84b62e5..b8c5c33 100755 --- a/src/backend/pyShelf_ScanLibrary.py +++ b/src/backend/pyShelf_ScanLibrary.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import os import sys import time diff --git a/src/interface/migrations/0005_navigation.py b/src/interface/migrations/0005_navigation.py new file mode 100644 index 0000000..fcae6e3 --- /dev/null +++ b/src/interface/migrations/0005_navigation.py @@ -0,0 +1,35 @@ +# Generated by Django 3.0.4 on 2020-06-10 05:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("interface", "0004_collections"), + ] + + operations = [ + migrations.CreateModel( + name="Navigation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("link", models.CharField(max_length=255, null=True)), + ("category", models.CharField(max_length=255, null=True)), + ("parent_id", models.IntegerField(null=True)), + ("alt", models.CharField(max_length=255, null=True)), + ("type", models.IntegerField(null=True)), + ("socket", models.CharField(max_length=255)), + ], + options={"db_table": "navigation",}, + ), + ] diff --git a/src/interface/models.py b/src/interface/models.py index 2f07909..5d78fd1 100755 --- a/src/interface/models.py +++ b/src/interface/models.py @@ -29,6 +29,12 @@ class Books(models.Model): pages = models.IntegerField(null=True) progress = models.IntegerField(null=True) file_name = models.CharField(max_length=255, null=False) + description = models.TextField(null=True) + identifier = models.CharField(max_length=255, null=True) + publisher = models.CharField(max_length=266, null=True) + date = models.DateField(null=True) + rights = models.CharField(max_length=255, null=True) + tags = models.CharField(max_length=255, null=True) def generic_search(self, query): try: @@ -62,3 +68,100 @@ class Collections(models.Model): except Exception as e: raise return results + + +class Navigation(models.Model): + """ + pyShelfs Navigation Database class + :param title: Link Text + :param link: Link link :) + :param category: Where in the nav tree do I belong + :param parent_id: This link is a sub link of link with id of me + :param alt: Alternate text of link + :param type: Web link, or Socket link which will be expected to act on \ + the link, and the action defined in socket + :param socket: if a Socket link define socket here + """ + + class Meta: + db_table = "navigation" + + def __str__(self): + return self.title + + title = models.CharField(max_length=255) + link = models.CharField(max_length=255, null=True) + category = models.CharField(max_length=255, null=True) + parent_id = models.IntegerField(null=True, editable=True) + alt = models.CharField(max_length=255, null=True) + type = models.IntegerField(null=True) + socket = models.CharField(max_length=255, null=False) + + def generic_search(self, query): + try: + results = Navigation.objects.annotate( + search=SearchVector("title", "parent_id", "category"), + ).filter(search=query) + except Exception as e: + raise + return results + + +class Users(models.Model): + """ + pyShelfs User Database class + :param uname: User Name + :param fname: First Name + :param lname: Last Name + :param email: User Email Address + :param password: User Password + :param ulvl: User Level + """ + + class Meta: + db_table = "users" + + def __str__(self): + return self.title + + uname = models.CharField(max_length=255) + fname = models.CharField(max_length=255, null=True) + lname = models.CharField(max_length=255, null=True) + email = models.CharField(max_length=255, null=True, editable=True) + password = models.CharField(max_length=255, null=True) + ulvl = models.IntegerField(null=True) + + def generic_search(self, query): + try: + results = Users.objects.annotate( + search=SearchVector("uname", "email", "lname"), + ).filter(search=query) + except Exception as e: + raise + return results + + +class Favorites(models.Model): + """ + pyShelfs User Database class + :param uname: User Name + :param fname: First Name + """ + + class Meta: + db_table = "favorites" + + def __str__(self): + return self.title + + favorite = models.ManyToManyField(Books) + uname = models.ManyToManyField(Users) + + def generic_search(self, query): + try: + results = Favorites.objects.annotate(search=SearchVector("uname"),).filter( + search=query + ) + except Exception as e: + raise + return results diff --git a/src/interface/static/css/main.css b/src/interface/static/css/main.css index 060213a..e824537 100755 --- a/src/interface/static/css/main.css +++ b/src/interface/static/css/main.css @@ -1,18 +1,17 @@ body { margin: 0px; padding: 0px; - background-color: #DCDCDD; - color: #fff; + background-color: #FFF; + color: #000; overflow-x: hidden; } #app { display: grid; - grid-template-areas: "app_header" - "app_body" - "app_footer"; - grid-template-rows: auto auto auto; - /*max-height: 100%;*/ + grid-template-areas: + "app_body"; + grid-template-rows: auto; + /*! max-height: 100%; */ } .clear { @@ -24,43 +23,44 @@ body { margin: 0px; display: grid; grid-template-areas: - "title slogan subhdr" - "nav_left_top nav_center_top nav_right_top"; - align-items: center; - background-color: #2b2b2b; + "title nav_left_top nav_center_top nav_right_top"; + grid-template-columns: 225px auto auto; padding: 4px 0px 4px; - grid-auto-columns: auto; + background-color: #2d2d2d; } .nav_left_top { grid-area: nav_left_top; display: flex; justify-content: left; + align-items: center; } .nav_center_top { grid-area: nav_center_top; display: flex; justify-content: center; + align-items: center; } .nav_right_top { grid-area: nav_right_top; display: flex; justify-content: flex-end; + align-items: center; } .app_hdr { grid-area: title; - margin: 0px 0px 5px 0px; + margin: 0px 0px 0px 0px; font-family: 'Gruppo', cursive; - font-size: 36px; + font-size: 20px; text-align: start; - padding: 0px 0px 0px 5px; + padding: 0px 0px 0px 0px; } .shadow { - text-shadow: #4c5c68 -5px 3px 5px; + text-shadow: #fff -1px 0px 11px; } .app_subhdr { @@ -84,24 +84,25 @@ body { grid-area: app_body; grid-template-rows: auto; grid-template-areas: "nav_l shelf"; - grid-template-columns: 15vw 85vw; - background-color: dimgray; + grid-template-columns: 160px auto; + background-color: white; } .nav_l { display: grid; grid-area: nav_l; font-family: 'Gruppo', cursive; - font-size: 20px; - max-height: 500px; - overflow-y: scroll; - padding: 0px 10px; + font-size: 15px; + /*! max-height: 500px; */ + overflow-y: auto; + /*! padding: 0px 10px; */ + margin: 0px; } .popover{ display: none; z-index: 100; - background-color: #000; + background-color: #cecece; /*min-width: 200px;*/ min-height: 30px; position: fixed; @@ -113,31 +114,35 @@ body { padding: 0px 10px; } .nav_l_hdr { - text-align: center; - padding: 5px; - background-color: #292f35; - border-bottom: 2px solid #000; + /*! text-align: center; */ + padding: 0px 5px; + background-color: #9e9e9e; + border-bottom: 2px solid #dadada; + color: black; + font-weight: bold; } .nav_l_0 { - background-color: #2b2b2b; - padding: 5px; - text-align: center; - border-bottom: 1px solid #000; + background-color: #dadada; + /*! padding: 5px; */ + /*! text-align: center; */ + border-bottom: 1px solid #dadada; + padding: 0px 0px 0px 10px; } .nav_l_1 { - padding: 5px; - text-align: center; - border-bottom: 1px solid #000; + /*! padding: 5px; */ + /*! text-align: center; */ + border-bottom: 1px solid #dadada; + padding: 0px 0px 0px 10px; } .nav_link {} #vert-nav { list-style: None; padding: 0px; - margin: 10px 0px; - border-left: 5px solid #292f35; - border-right: 5px solid #292f35; + margin: 0px; + /*! border-left: 5px solid #292f35; */ + /*! border-right: 5px solid #292f35; */ } .vert-nav-item {} @@ -221,16 +226,18 @@ body { background-color: darkgray; border-radius: 5px; border: 1px solid #999; - min-width: 110px; + /*! min-width: 110px; */ margin: 0px 5px 0px 0px; - padding-top: 2px; - padding-bottom: 2px; + /*! padding-top: 2px; */ + /*! padding-bottom: 2px; */ + /*! max-height: 20px; */ } .nav_search { margin: 0px 5px 0px 0px; border-radius: 5px; border: 1px solid #999; + max-height: 17px; } .search {} @@ -273,11 +280,9 @@ p { } #book_shelf { - display: grid; - grid-template-columns: 21% 21% 21% 21%; list-style-type: none; font-family: 'Audiowide', cursive; - font-size: 25px; + font-size: 15px; padding: 0; margin: 0px; min-width: 99vw; @@ -285,23 +290,34 @@ p { .shelf_item { display: grid; - background-color: burlywood; + grid-template-columns: 100px auto; + grid-template-areas: "thumb details"; margin: 0 10px 10px 10px; - max-width: 20vw; - max-height: 70vh; - text-align: center; } .book_thumb { - width: 20vw; - height: 70vh; + grid-area: thumb; + width: 100px; } a.book_link { text-decoration: none; } - +.book_details {} +.book_details_list {grid-area: details;list-style-type: none;} +.book_title {} +.book_author {} +.book_controls {} a.nav_link { text-decoration: none; - color: #fff; + color: #000; } +.hidden{ + display: none; +} +.vert-nav{ + list-style: None; + padding: 0px; + margin: 0px 0px; +} +.btn {cursor:pointer} diff --git a/src/interface/static/js/pyshelf_ux.js b/src/interface/static/js/pyshelf_ux.js index 64374cf..8bad8e5 100755 --- a/src/interface/static/js/pyshelf_ux.js +++ b/src/interface/static/js/pyshelf_ux.js @@ -30,9 +30,9 @@ $(document).ready(function(){ $(this).removeAttr("disabled"); } }); - $('#app').css("height", max_height); - $('.nav_l').css("max-height", max_height); - $('div.shelf').css("max-height", max_height); +// $('#app').css("height", max_height); +// $('.nav_l').css("max-height", max_height); +// $('div.shelf').css("max-height", max_height); $('.nav_link').on('mouseover', function (e){ var popover_str = $(this).attr('alt'); x = $(this).offset().left @@ -51,4 +51,7 @@ $(document).ready(function(){ $('.popover').css('top', y); $('.popover').css('display','none'); }); + $('#btn_collections').on('click', function (e){ + $('.hidden.vert-nav.collections').toggle() + }); }) diff --git a/src/interface/templates/index.html b/src/interface/templates/index.html index d8ea54b..888a2ac 100755 --- a/src/interface/templates/index.html +++ b/src/interface/templates/index.html @@ -17,76 +17,75 @@ +
-
-
-

pyShelf {{Version}}

-
-
-

"An elegant tool... for a more civilized age."

-
-
- - - - - Fork - - Issue - -
- - - - -
    {% for book in Books %} - -
  • {{ book.title }}
  • -
    +
  • + cover missing +
      +
    • {{ book.title }}
    • +
    • Author: {{ book.author }}
    • +
    • + fav + download + share +
    • +
    +
  • {% endfor %}
-
diff --git a/src/interface/templates/search.html b/src/interface/templates/search.html index 2ca7c70..3ed5ce0 100755 --- a/src/interface/templates/search.html +++ b/src/interface/templates/search.html @@ -16,48 +16,44 @@
-
-
-

pyShelf {{Version}}

-
-
-

"An elegant tool... for a more civilized age."

-
-
- - - - - Fork - -Issue - -
- - - - -
+
@@ -73,17 +69,6 @@
- diff --git a/src/interface/views.py b/src/interface/views.py index 590bb39..c59abe7 100755 --- a/src/interface/views.py +++ b/src/interface/views.py @@ -9,7 +9,7 @@ from django.http import JsonResponse from django.shortcuts import HttpResponse, render # render_to_response from django.utils.text import slugify -from .models import Books, Collections +from .models import Books, Collections, Navigation config = Config(Path("../")) @@ -26,7 +26,8 @@ def index(request): "Books": book_set(20, _set), "Set": str(_set), "Version": config.VERSION, - "LeftNav": menu("collections"), + "LeftNavCollections": menu("collections"), + "LeftNavMenu0": menu("nav_l_0"), }, ) @@ -43,6 +44,7 @@ def show_collection(request, _collection, _colset): "Books": collection(_collection, _set), "Set": str(_set), "Version": config.VERSION, + "LeftNavCollections": menu("collections"), "LeftNav": menu("collections"), }, ) @@ -63,6 +65,7 @@ def next_page(request, bookset): "Books": book_set(None, _set), "Set": str(_set), "Version": config.VERSION, + "LeftNavCollections": menu("collections"), "LeftNav": menu("collections"), }, ) @@ -87,6 +90,7 @@ def prev_page(request, bookset): "Books": book_set(None, _set), "Set": str(_set), "Version": config.VERSION, + "LeftNavCollections": menu("collections"), "LeftNav": menu("collections"), }, ) @@ -117,6 +121,7 @@ def search(request, query=None, _set=1, _limit=None): "Set": _set, "len_results": search_len, "Version": config.VERSION, + "LeftNavCollections": menu("collections"), "LeftNav": menu("collections"), }, ) @@ -190,7 +195,21 @@ def hr_name(book): return "{0}{1}".format(slugify(book.title), os.path.splitext(book.file_name)[1]) -def menu(which, _set=1): +def format_list(list_in): + formated_list, formated_list_key, x = [], [], 0 + for i in list_in: + if i.id not in formated_list_key: + if x % 2 == 0: + c = 0 + else: + c = 1 + if x <= 10: + x += 1 + else: + x = 0 + + +def menu(which, _set=1, parent=None): if which == "collections": collection_list = Collections.objects.all() collections, collection_key, x = [], [], 0 @@ -217,3 +236,7 @@ def menu(which, _set=1): ) collection_key.append(i.collection) return collections + elif which == "nav_lvl_0": + navigation_list = Navigation.objects.all() + breakpoint() + return navigation_list