FrederikBaerentsen
2 years ago
commit
a25dff10ec
12 changed files with 422 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
FROM python:3.8 |
|||
RUN mkdir /app |
|||
WORKDIR /app |
|||
ADD . /app/ |
|||
RUN pip install -r requirements.txt |
|||
EXPOSE 5000 |
|||
ENV FLASK_APP=main |
|||
CMD ["python", "main.py"] |
@ -0,0 +1,16 @@ |
|||
import os |
|||
from werkzeug.security import generate_password_hash |
|||
|
|||
#CONTENT_BASE_DIR = os.getenv("CONTENT_BASE_DIR", "/library") |
|||
CONTENT_BASE_DIR = os.getenv("CONTENT_BASE_DIR", "/home/drudoo/ComicsTest/Comics") |
|||
|
|||
TEENYOPDS_ADMIN_PASSWORD = os.getenv("TEENYOPDS_ADMIN_PASSWORD", None) |
|||
users = {} |
|||
if TEENYOPDS_ADMIN_PASSWORD: |
|||
users = { |
|||
"admin": generate_password_hash(TEENYOPDS_ADMIN_PASSWORD), |
|||
} |
|||
else: |
|||
print( |
|||
"WANRNING: admin password not configured - catalog will be exposed was public" |
|||
) |
@ -0,0 +1,89 @@ |
|||
from flask import Flask, send_from_directory, request |
|||
from flask_httpauth import HTTPBasicAuth |
|||
from werkzeug.security import check_password_hash |
|||
from gevent.pywsgi import WSGIServer |
|||
import timeit |
|||
import sqlite3 |
|||
import os |
|||
import zipfile |
|||
from bs4 import BeautifulSoup |
|||
import re |
|||
import datetime |
|||
import sys |
|||
|
|||
from opds import fromdir |
|||
import config |
|||
|
|||
app = Flask(__name__, static_url_path="", static_folder="static") |
|||
auth = HTTPBasicAuth() |
|||
|
|||
|
|||
@auth.verify_password |
|||
def verify_password(username, password): |
|||
if not config.TEENYOPDS_ADMIN_PASSWORD: |
|||
return True |
|||
elif username in config.users and check_password_hash( |
|||
config.users.get(username), password |
|||
): |
|||
return username |
|||
|
|||
|
|||
@app.route("/") |
|||
@app.route("/healthz") |
|||
def healthz(): |
|||
return "ok" |
|||
|
|||
@app.route('/import') |
|||
def import2sql(): |
|||
conn = sqlite3.connect('app.db') |
|||
list = [] |
|||
|
|||
for root, dirs, files in os.walk(os.path.abspath(config.CONTENT_BASE_DIR)): |
|||
for file in files: |
|||
f = os.path.join(root, file) |
|||
s = zipfile.ZipFile(f) |
|||
Bs_data = BeautifulSoup(s.open('ComicInfo.xml').read(), "xml") |
|||
#print(Bs_data.select('Series')[0].text, file=sys.stderr) |
|||
#print(Bs_data.select('Title')[0].text, file=sys.stderr) |
|||
CVDB=re.findall('(?<=\[CVDB)(.*)(?=].)', Bs_data.select('Notes')[0].text) |
|||
#list.append('CVDB'+CVDB[0] + ': ' + Bs_data.select('Series')[0].text + "(" + Bs_data.select('Volume')[0].text + ") : " + Bs_data.select('Number')[0].text ) |
|||
#print(list, file=sys.stdout) |
|||
|
|||
ISSUE=Bs_data.select('Number')[0].text |
|||
SERIES=Bs_data.select('Series')[0].text |
|||
VOLUME=Bs_data.select('Volume')[0].text |
|||
PUBLISHER=Bs_data.select('Publisher')[0].text |
|||
TITLE=Bs_data.select('Title')[0].text |
|||
PATH=f |
|||
UPDATED=str(datetime.datetime.now()) |
|||
print(UPDATED,file=sys.stdout) |
|||
sql="INSERT OR REPLACE INTO COMICS (CVDB,ISSUE,SERIES,VOLUME, PUBLISHER, TITLE, FILE,PATH,UPDATED) VALUES ("+CVDB[0]+",'"+ISSUE+"','"+SERIES+"','"+VOLUME+"','"+PUBLISHER+"','"+TITLE+"','"+file+"','" + f + "','" + UPDATED + "')" |
|||
print(sql,file=sys.stdout) |
|||
conn.execute(sql); |
|||
conn.commit() |
|||
|
|||
conn.close() |
|||
return "yay" |
|||
|
|||
@app.route("/content/<path:path>") |
|||
@auth.login_required |
|||
def send_content(path): |
|||
return send_from_directory(config.CONTENT_BASE_DIR, path) |
|||
|
|||
@app.route("/catalog") |
|||
@app.route("/catalog/<path:path>") |
|||
@auth.login_required |
|||
def catalog(path=""): |
|||
start_time = timeit.default_timer() |
|||
print(request.root_url) |
|||
c = fromdir(request.root_url, request.url, config.CONTENT_BASE_DIR, path) |
|||
elapsed = timeit.default_timer() - start_time |
|||
print(elapsed) |
|||
|
|||
return c.render() |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
#http_server = WSGIServer(("", 5000), app) |
|||
#http_server.serve_forever() |
|||
app.run(debug=True,host='0.0.0.0') |
@ -0,0 +1,16 @@ |
|||
import requests |
|||
|
|||
|
|||
def fromisbn(isbn: str): |
|||
isbn = "".join(filter(str.isnumeric, isbn)) |
|||
api = f"https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn}" |
|||
resp = requests.get(api) |
|||
return resp.json()["items"][0] |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
from pprint import pprint |
|||
|
|||
pprint(fromisbn("9780316029193")) |
|||
pprint(fromisbn("978-0316029193")) |
|||
pprint(fromisbn("0316029193")) |
@ -0,0 +1 @@ |
|||
from .catalog import Catalog, fromdir |
@ -0,0 +1,136 @@ |
|||
import os |
|||
from uuid import uuid4 |
|||
from urllib.parse import quote |
|||
from jinja2 import Environment, FileSystemLoader, select_autoescape |
|||
from .entry import Entry |
|||
from .link import Link |
|||
import sqlite3 |
|||
|
|||
|
|||
class Catalog(object): |
|||
def __init__( |
|||
self, |
|||
title, |
|||
id=None, |
|||
author_name=None, |
|||
author_uri=None, |
|||
root_url=None, |
|||
url=None, |
|||
): |
|||
self.title = title |
|||
self.id = id or uuid4() |
|||
self.author_name = author_name |
|||
self.author_uri = author_uri |
|||
self.root_url = root_url |
|||
self.url = url |
|||
self.entries = [] |
|||
|
|||
def add_entry(self, entry): |
|||
self.entries.append(entry) |
|||
|
|||
def render(self): |
|||
env = Environment( |
|||
loader=FileSystemLoader( |
|||
searchpath=os.path.join(os.path.dirname(__file__), "templates") |
|||
), |
|||
autoescape=select_autoescape(["html", "xml"]), |
|||
) |
|||
template = env.get_template("catalog.opds.jinja2") |
|||
return template.render(catalog=self) |
|||
|
|||
def fromsearch(root_url, url, content_base_path, content_relative_path): |
|||
|
|||
c = Catalog( |
|||
title="test" |
|||
) |
|||
|
|||
return c |
|||
|
|||
def fromdir(root_url, url, content_base_path, content_relative_path): |
|||
|
|||
path = os.path.join(content_base_path, content_relative_path) |
|||
#print(path) |
|||
c = Catalog( |
|||
title=os.path.basename(os.path.dirname(path)), root_url=root_url, url=url |
|||
) |
|||
#print(c.url) |
|||
if not "search" in c.url: |
|||
onlydirs = [ |
|||
f for f in os.listdir(path) if not os.path.isfile(os.path.join(path, f)) |
|||
] |
|||
#print(onlydirs) |
|||
for dirname in onlydirs: |
|||
link = Link( |
|||
href=quote(f"/catalog/{content_relative_path}/{dirname}"), |
|||
rel="subsection", |
|||
rpath=path, |
|||
type="application/atom+xml;profile=opds-catalog;kind=acquisition", |
|||
) |
|||
c.add_entry(Entry(title=dirname, id=uuid4(), links=[link])) |
|||
|
|||
|
|||
if c.url.endswith("/catalog"): |
|||
link2 = Link( |
|||
href=quote(f"/catalog/search"), |
|||
rel="subsection", |
|||
rpath=path, |
|||
type="application/atom+xml;profile=opds-catalog;kind=acquisition", |
|||
) |
|||
c.add_entry(Entry(title="Search",id=uuid4(),links=[link2])) |
|||
|
|||
if not "search" in c.url: |
|||
onlyfiles = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] |
|||
#print(onlyfiles) |
|||
for filename in onlyfiles: |
|||
link = Link( |
|||
href=quote(f"/content/{content_relative_path}/{filename}"), |
|||
rel="http://opds-spec.org/acquisition", |
|||
rpath=path, |
|||
type=mimetype(filename), |
|||
) |
|||
c.add_entry(Entry(title=filename.rsplit(".",1)[0], id=uuid4(), links=[link])) |
|||
#fixed issue with multiple . in filename |
|||
#print(c.render()) |
|||
else: |
|||
search="Man" |
|||
conn = sqlite3.connect('app.db') |
|||
sql="SELECT * from COMICS where SERIES like '%" + search+ "%' or Title like '%" + search+ "%';" |
|||
|
|||
s = conn.execute(sql) |
|||
list=[] |
|||
for r in s: |
|||
#print(r) |
|||
tUrl=f""+r[7].replace("/home/drudoo/ComicsTest/Comics/","/content/") |
|||
tTitle=r[6] |
|||
link3 = Link( |
|||
#href=quote(f"/content/DC Comics/Earth Cities/Gotham City/Batgirl/Annual/(2012) Batgirl Annual/Batgirl Annual #001 - The Blood That Moves Us [December, 2012].cbz"), |
|||
href=quote(tUrl), |
|||
rel="http://opds-spec.org/acquisition", |
|||
rpath=path, |
|||
type="application/x-cbz", |
|||
) |
|||
c.add_entry( |
|||
Entry( |
|||
title=tTitle, |
|||
id=uuid4(), |
|||
links=[link3] |
|||
) |
|||
) |
|||
|
|||
|
|||
return c |
|||
|
|||
|
|||
|
|||
def mimetype(path): |
|||
extension = path.split(".")[-1].lower() |
|||
if extension == "pdf": |
|||
return "application/pdf" |
|||
elif extension == "epub": |
|||
return "application/epub" |
|||
elif extension == "mobi": |
|||
return "application/mobi" |
|||
elif extension == "cbz": |
|||
return "application/x-cbz" |
|||
else: |
|||
return "application/unknown" |
@ -0,0 +1,77 @@ |
|||
import zipfile |
|||
from bs4 import BeautifulSoup |
|||
import os |
|||
|
|||
class Entry(object): |
|||
valid_keys = ( |
|||
"id", |
|||
"url", |
|||
"title", |
|||
"content", |
|||
"downloadsPerMonth", |
|||
"updated", |
|||
"identifier", |
|||
"date", |
|||
"rights", |
|||
"summary", |
|||
"dcterms_source", |
|||
"provider", |
|||
"publishers", |
|||
"contributors", |
|||
"languages", |
|||
"subjects", |
|||
"oai_updatedates", |
|||
"authors", |
|||
"formats", |
|||
"links", |
|||
) |
|||
|
|||
required_keys = ("id", "title", "links") |
|||
|
|||
def validate(self, key, value): |
|||
if key not in Entry.valid_keys: |
|||
raise KeyError("invalid key in opds.catalog.Entry: %s" % (key)) |
|||
|
|||
def __init__(self, **kwargs): |
|||
for key, val in kwargs.items(): |
|||
self.validate(key, val) |
|||
|
|||
for req_key in Entry.required_keys: |
|||
if not req_key in kwargs: |
|||
raise KeyError("required key %s not supplied for Entry!" % (req_key)) |
|||
self.id = kwargs["id"] |
|||
self.title = kwargs["title"] |
|||
self.links = kwargs["links"] |
|||
self._data = kwargs |
|||
|
|||
#print(">>entry.py") |
|||
#print(kwargs) |
|||
#print(kwargs["links"][0].get("rpath")) |
|||
#print("--end entry.py") |
|||
|
|||
if kwargs["links"][0].get("type") == 'application/x-cbz': |
|||
f=self.links[0].get("rpath")+"/"+self.title+".cbz" |
|||
if os.path.exists(f): |
|||
s = zipfile.ZipFile(f) |
|||
data=BeautifulSoup(s.open('ComicInfo.xml').read(), "xml") |
|||
#print(data) |
|||
#print(kwargs["links"][0]) |
|||
#print(data.select('Series')[0].text) |
|||
#print(kwargs["links"][0].get("rpath")) |
|||
if data.select('Series')[0].text in kwargs["links"][0].get("rpath"): |
|||
releasedate=data.select('Year')[0].text+"-"+data.select('Month')[0].text.zfill(2)+"-"+data.select('Day')[0].text.zfill(2) |
|||
self.title = "#"+data.select('Number')[0].text.zfill(2) + ": " + data.select('Title')[0].text + " (" + releasedate + ")" |
|||
#print(self.title) |
|||
else: |
|||
self.title = kwargs["title"] |
|||
else: |
|||
self.title = kwargs["title"] |
|||
#self.title = data.select('Title')[0].text |
|||
|
|||
|
|||
def get(self, key): |
|||
return self._data.get(key, None) |
|||
|
|||
def set(self, key, value): |
|||
self.validate(key, value) |
|||
self._data[key] = value |
@ -0,0 +1,31 @@ |
|||
class Link(object): |
|||
valid_keys = ("href", "type", "rel", "rpath", "price", "currencycode", "formats") |
|||
required_keys = ("href", "type", "rel") |
|||
|
|||
def validate(self, key, value): |
|||
if key not in Link.valid_keys: |
|||
raise KeyError("invalid key in opds.Link: %s" % (key)) |
|||
|
|||
def __init__(self, **kwargs): |
|||
for key, val in kwargs.items(): |
|||
self.validate(key, val) |
|||
|
|||
for req_key in Link.required_keys: |
|||
if not req_key in kwargs: |
|||
raise KeyError("required key %s not supplied for Link!" % (req_key)) |
|||
|
|||
self.href = kwargs["href"] |
|||
self.type = kwargs["type"] |
|||
self.rel = kwargs["rel"] |
|||
self._data = kwargs |
|||
|
|||
#print(">>link.py") |
|||
#print(kwargs) |
|||
#print("--end link.py") |
|||
|
|||
def get(self, key): |
|||
return self._data.get(key, None) |
|||
|
|||
def set(self, key, value): |
|||
self.validate(key, value) |
|||
self._data[key] = value |
@ -0,0 +1,39 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<feed xmlns="http://www.w3.org/2005/Atom" |
|||
xmlns:dc="http://purl.org/dc/terms/" |
|||
xmlns:ov="http://open.vocab.org/terms/" |
|||
xmlns:oz="http://openzim.org/terms/" |
|||
xmlns:opds="http://opds-spec.org/2010/catalog"> |
|||
<id>urn:uuid:{{ catalog.id }}</id> |
|||
<title>{{ catalog.title }}</title> |
|||
{% if catalog.author_name or catalog.author_url %} |
|||
<author> |
|||
{% if catalog.author_name %} |
|||
<name>{{ catalog.author_name }}</name> |
|||
{% endif %} |
|||
{% if catalog.author_url %} |
|||
<uri>{{ catalog.author_url }}</uri> |
|||
{% endif %} |
|||
</author> |
|||
{% endif %} |
|||
<link rel="start" |
|||
href="{{ catalog.root_url }}" |
|||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> |
|||
<link rel="self" |
|||
href="{{ catalog.url }}" |
|||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> |
|||
|
|||
{% for entry in catalog.entries %} |
|||
<entry> |
|||
<title>{{ entry.title }}</title> |
|||
<id>{{ entry.id }}</id> |
|||
{% if entry.updated %} <updated>{{ entry.updated }}</updated> {% endif %} |
|||
{% for link in entry.links %} |
|||
<link rel="{{ link.rel }}" |
|||
href="{{ link.href }}" |
|||
type="{{ link.type }}"/> |
|||
{% endfor %} |
|||
</entry> |
|||
{% endfor %} |
|||
|
|||
</feed> |
@ -0,0 +1,7 @@ |
|||
Flask==2.0.2 |
|||
Jinja2==3.0.2 |
|||
requests==2.26.0 |
|||
Flask-HTTPAuth==4.5.0 |
|||
gevent==21.8.0 |
|||
bs4 |
|||
lxml |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,2 @@ |
|||
User-agent: * |
|||
Disallow: / |
Loading…
Reference in new issue