Browse Source

initial commit

master
FrederikBaerentsen 2 years ago
commit
a25dff10ec
  1. 8
      Dockerfile
  2. 16
      config.py
  3. 89
      main.py
  4. 16
      metadata.py
  5. 1
      opds/__init__.py
  6. 136
      opds/catalog.py
  7. 77
      opds/entry.py
  8. 31
      opds/link.py
  9. 39
      opds/templates/catalog.opds.jinja2
  10. 7
      requirements.txt
  11. BIN
      static/favicon.ico
  12. 2
      static/robots.txt

8
Dockerfile

@ -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"]

16
config.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"
)

89
main.py

@ -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')

16
metadata.py

@ -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"))

1
opds/__init__.py

@ -0,0 +1 @@
from .catalog import Catalog, fromdir

136
opds/catalog.py

@ -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"

77
opds/entry.py

@ -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

31
opds/link.py

@ -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

39
opds/templates/catalog.opds.jinja2

@ -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>

7
requirements.txt

@ -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

BIN
static/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
static/robots.txt

@ -0,0 +1,2 @@
User-agent: *
Disallow: /
Loading…
Cancel
Save