29 Commits
v0.1.1 ... v2.0

Author SHA1 Message Date
eneller
d1ed7ec389 doc: readme 2025-04-05 16:53:18 +02:00
eneller
bceaf6904a feat: sytling 2025-04-05 16:33:46 +02:00
eneller
ee598e4810 fix: pagination when searching 2025-04-05 16:14:39 +02:00
eneller
6e7d6b1e2a feat: pagination 2025-04-05 13:38:18 +02:00
eneller
1ef440fb8f feat: display number of search results 2025-04-05 10:28:08 +02:00
eneller
21d6d265ed feat: server search 2025-04-05 10:10:41 +02:00
eneller
41f7836a48 feat: encode every search in URI 2025-04-05 09:40:25 +02:00
eneller
eab2268bab build: upgrade uv.lock 2025-04-05 09:23:33 +02:00
eneller
09f55f8ea5 feat: ui tweaks 2025-04-05 09:15:12 +02:00
eneller
e286ca4f02 doc: readme 2025-04-05 03:14:40 +02:00
eneller
60ad75ecb0 feat: nginx config 2025-04-04 23:18:15 +00:00
eneller
54eda4f9ea usable celery dev setup
commit 5438243b4874879196294a4072f245ebf22afd5d
Author: eneller <erikneller@gmx.de>
Date:   Sat Apr 5 00:12:55 2025 +0200

    minor celery

commit 3a03c498a7e6acff1afe350ce1ae196375d19029
Author: eneller <erikneller@gmx.de>
Date:   Fri Apr 4 23:40:23 2025 +0200

    begin celery
2025-04-05 00:15:11 +02:00
eneller
20de18fa13 build: upgrade uv.lock 2025-04-02 15:21:57 +02:00
eneller
ea3942651e feat: add waiting page 2025-03-24 12:04:04 +01:00
eneller
04103b82a2 chore: modular template 2025-03-24 11:48:05 +01:00
eneller
41eabd6026 chore: prepare for template hierarchy 2025-03-24 11:35:41 +01:00
eneller
24c986cbe9 feat: clickable title 2025-03-23 23:24:45 +01:00
eneller
9fe5d3dcde fix: sidescrolling 2025-03-23 23:14:15 +01:00
eneller
a2390e4359 minor fixes 2025-03-22 18:26:22 +01:00
eneller
66809ff88c remove django admin app 2025-03-21 20:37:26 +01:00
eneller
af36e86059 begin gunicorn 2025-03-21 20:17:38 +01:00
eneller
823639588a feat: favicon, webmanifest 2025-03-21 19:43:34 +01:00
eneller
82fea63583 fix: table headers 2025-03-20 22:20:09 +01:00
eneller
c728f65e73 fix: search 2025-03-20 21:46:53 +01:00
eneller
c1f813d70c frontend tweaks 2025-03-20 20:53:19 +01:00
eneller
aa66c06f88 feat: CTRL + K search, css 2025-03-20 20:15:51 +01:00
eneller
518a8a1744 minor tweaks 2025-03-20 15:07:06 +01:00
eneller
b65d878981 build: add watchman 2025-03-19 22:25:23 +01:00
eneller
3d10081846 feat: error handling 2025-03-19 22:04:23 +01:00
27 changed files with 440 additions and 189 deletions

1
.gitignore vendored
View File

@@ -372,3 +372,4 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,django,visualstudiocode,intellij+all # End of https://www.toptal.com/developers/gitignore/api/python,django,visualstudiocode,intellij+all
staticfiles/

View File

@@ -1,2 +1,35 @@
# epub2go-web # epub2go-web
A simple Website to provide a `NNI (Non-Nerd Interface)` to [epub2go.py](https://github.com/eneller/epub2go.py), a web to epub converter. A simple Website to provide a `NNI (Non-Nerd Interface)` to [epub2go.py](https://github.com/eneller/epub2go.py), a web to epub converter.
## Development
This project uses [watchman](https://facebook.github.io/watchman/) for file watching and reloading.
Follow the [official instructions](https://facebook.github.io/watchman/docs/install.html) for your system to install, django will default to its standard watcher otherwise.
To run the server, install the dependencies described in `pyproject.toml` and `uv.lock`
in a virtual environment, ideally using [uv](https://docs.astral.sh/uv/) (or pip).
[Celery](https://docs.celeryq.dev/en/stable/) is used as a task queue with [redis](https://hub.docker.com/_/redis) as backend.
A container for it is provided in the `docker-compose.yml`.
After the container is up, simply start your Celery workers from `src/` using
```bash
celery -A epub2go_web worker --loglevel=INFO
```
Finally, run the **development** server using
```bash
python manage.py runserver
```
## Deployment
Follow the Development instructions, except replace the final command for the development server with
```bash
gunicorn -c gunicorn.py
```
[Gunicorn](https://gunicorn.org/) does not serve static files and is intended to be deployed behind [nginx](https://nginx.org/).
An example configuration is provided in `nginx.conf`.
To collect the static files for nginx, run
```bash
python manage.py collectstatic
```
and point nginx to the resulting folder.

6
docker-compose.yml Normal file
View File

@@ -0,0 +1,6 @@
name: task-queue
services:
redis:
ports:
- 6379:6379
image: redis

15
nginx.conf Normal file
View File

@@ -0,0 +1,15 @@
server {
listen 80;
server_name epub2go.example.org;
access_log /var/log/nginx/epub2go.log;
location /static {
alias /www/epub2go;
}
location / {
proxy_pass http://127.0.0.1:50000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

View File

@@ -5,9 +5,13 @@ description = "Web Interface to epub2go.py"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"celery>=5.4.0", "celery[redis]>=5.4.0",
"django>=5.1.6", "django>=5.1.6",
"epub2go", "epub2go",
"gunicorn>=23.0.0",
"python-dotenv>=1.0.1",
"pywatchman>=2.0.0",
"redis>=5.2.1",
] ]
[tool.uv.sources] [tool.uv.sources]

4
src/.watchmanconfig Normal file
View File

@@ -0,0 +1,4 @@
{
"ignore_dirs": ["node_modules"]
}

View File

@@ -1,50 +1,65 @@
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, FileResponse from django.http import HttpRequest, HttpResponse, FileResponse, HttpResponseBadRequest
from django.conf import settings from django.core.paginator import Paginator
from celery import shared_task
from epub2go.convert import get_all_books, Book, GBConvert, allbooks_url from epub2go.convert import get_all_books, Book, allbooks_url
import os import os
from urllib.parse import urlparse from urllib.parse import urlparse
import logging import logging
from epub2go_web.tasks import getEpub
logger = logging.getLogger(__name__) #TODO configure logging logger = logging.getLogger(__name__) #TODO configure logging
converter = GBConvert(downloaddir=settings.MEDIA_ROOT) books = sorted(get_all_books(), key= lambda b: b.title)# TODO get from pickle
books = get_all_books()# TODO get from pickle
gbnetloc = urlparse(allbooks_url).netloc gbnetloc = urlparse(allbooks_url).netloc
def index(request: HttpRequest): def index(request: HttpRequest):
context = { localbooks = books
'title': 'epub2go',
'http_host': request.META['HTTP_HOST'],
'books': books,
'book_count': len(books),
}
targetParam = request.GET.get('t', None) targetParam = request.GET.get('t', None)
searchParam = request.GET.get('s', None)
if targetParam:
if validateUrl(targetParam): if validateUrl(targetParam):
fpath = getEpub(targetParam) # download file
result = getEpub.delay(targetParam)
fpath = result.get(timeout=60)
fname = os.path.basename(fpath) fname = os.path.basename(fpath)
file = open(fpath, 'rb') file = open(fpath, 'rb')
response = FileResponse(file) response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream' response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = f'attachment; filename="{fname}"' response['Content-Disposition'] = f'attachment; filename="{fname}"'
return response return response
else: return HttpResponseBadRequest('Input URL invalid.')
elif searchParam:
localbooks = [book for book in books if searchParam in book.title]
return render(request, 'index.html', context) # paginate items
paginationParam = request.GET.get('p', '')
try:
pageNo = int(paginationParam)
except ValueError:
logger.error('Failed to cast %s to int', paginationParam)
pageNo = 1
pages = Paginator(localbooks, 100)
if pageNo < 1 or pageNo > pages.num_pages:
pageNo = 1
page = pages.page(pageNo)
context = {
'title': 'epub2go',
'http_host': f'http://{ request.META['HTTP_HOST'] }',
'page': page,
'allbooks_count': len(books),
'allbooks_url': allbooks_url,
}
# return base view
return render(request, 'home.html', context)
def validateUrl(param)->bool : def validateUrl(param)->bool :
if not param: return False
netloc = urlparse(param).netloc netloc = urlparse(param).netloc
if(netloc == gbnetloc): return True if(netloc == gbnetloc): return True
return False return False
# TODO make this async and show some indication of progress/loading
#@shared_task
def getEpub(book_url):
# TODO check for existing file and age
return converter.download(book_url)

View File

@@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

23
src/epub2go_web/celery.py Normal file
View File

@@ -0,0 +1,23 @@
# Start Celery workers from src/ using `celery -A epub2go_web worker --loglevel=INFO`
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'epub2go_web.settings')
app = Celery('epub2go_web')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')

View File

@@ -20,20 +20,18 @@ PROJ_DIR = BASE_DIR / 'epub2go_web'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret! TODO
SECRET_KEY = "django-insecure-^@m5bl*8x+=@c^b0lhkgb-%_#9#&oad=v15jq=!0$g#x17zjf8" SECRET_KEY = "django-insecure-^@m5bl*8x+=@c^b0lhkgb-%_#9#&oad=v15jq=!0$g#x17zjf8"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['*']
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
@@ -46,7 +44,6 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
@@ -64,7 +61,6 @@ TEMPLATES = [
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
], ],
}, },
@@ -73,48 +69,18 @@ TEMPLATES = [
WSGI_APPLICATION = "epub2go_web.wsgi.application" WSGI_APPLICATION = "epub2go_web.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
#DATABASES = { # Celery settings
# "default": { CELERY_BROKER_URL = "redis://localhost:6379/0"
# "ENGINE": "django.db.backends.sqlite3", CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
# "NAME": BASE_DIR / "db.sqlite3", CELERY_TASK_ALWAYS_EAGER = True
# }
#}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
@@ -123,11 +89,7 @@ STATIC_URL = "static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [
PROJ_DIR / "static/", PROJ_DIR / "static/",
] ]
STATIC_ROOT = PROJ_DIR/ "staticfiles"
MEDIA_URL = "media/" MEDIA_URL = "media/"
MEDIA_ROOT = PROJ_DIR / "media/" MEDIA_ROOT = PROJ_DIR / "media/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -2,27 +2,29 @@
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const searchInput = document.getElementById('searchInput'); const searchInput = document.getElementById('searchInput');
const table = document.getElementById('table'); const table = document.getElementById('table');
const table_r = Array.from(table.getElementsByTagName('tr')); const table_r = Array.from(table.getElementsByClassName('table-entry'));
// allow search from url parameter document.addEventListener('keydown', (event)=>{
if (event.ctrlKey && event.key === 'k'){
event.preventDefault();
searchInput.focus();
searchInput.select();
}
});
// search from url parameter
let searchParam = params.get('s'); let searchParam = params.get('s');
if (searchParam){ if (searchParam){
searchInput.value = searchParam; searchInput.value = searchParam;
search(searchParam);
} }
function submitSearch(event){ function submitSearch(event){
event.preventDefault(); event.preventDefault();
search(); let path = window.location.pathname + "./?s="+encodeURIComponent(searchInput.value);
window.location.href= path;
} }
function search(searchStr = searchInput.value){
function showMatch(tr){ function buildURI(p){
// match search with list let params = new URLSearchParams(window.location.search);
let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent) params.set('p', p);
.join(' ') return `${window.location.pathname}?${params.toString()}`;
.indexOf(searchStr) > -1;
if (searchSuccess) tr.style.display = "";
else tr.style.display = "none";
}
table_r.map(showMatch, table_r);
} }

View File

@@ -0,0 +1,21 @@
{
"name": "epub2go Converter",
"short_name": "epub2go",
"icons": [
{
"src": "/static/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,49 +1,79 @@
/* this is part of the http://bettermotherfuckingwebsite.com/ */ /*TODO also style svg icons accordingly */
:root{
--bg:#faf0e673;
--bg-acc:#EEEEEE;
--bg-hover: #DDDDDD;
--fg:#444;
--fg-deemph: #777;
}
html{
font-family: serif;
}
body{ body{
margin:40px auto; background-color: var(--bg);
max-width:650px; overflow-x: hidden;
line-height:1.6; margin:.5em auto;
max-width:min(100%,800px);
line-height:1.4;
font-size:18px; font-size:18px;
color:#444; color:var(--fg);
padding:0 padding:0 .2em;
10px} justify-content: center;
}
h1,h2,h3{ h1,h2,h3{
line-height:1.2 line-height:1.2;
} letter-spacing: -2%;
/* custom styles here */
:root{
--white:#faf0e673;
}
body{
background-color: var(--white)
} }
header{ header{
text-align: center; text-align: center;
justify-content: center; justify-content: center;
} }
small{ small{
color: #777; color: var(--fg-deemph);
} }
.searchbar{ .search-bar{
width: fit-content; width: 100%;
}
.search-container{
text-align: center;
justify-content: center;
margin-bottom: 4%;
}
#searchInput {
background-color: var(--bg);
width: 100%;
padding: .2em;
font-size: larger;
border-radius: 10px;
border-color: var(--bg-acc);
box-shadow: var(--bg-acc) 2px 2px;
}
.main-banner{
text-align: center;
}
table, tr{
/* make table not resize when elements are hidden by searching */
width: 100%;
word-break: break-all;
}
th{
text-align: left;
} }
tr:nth-child(even){ tr:nth-child(even){
background-color: #EEEEEE; background-color: var(--bg-acc);
} }
tr:hover{ tr:hover{
background-color: #DDDDDD; background-color: var(--bg-hover);
transition: all 2ms; transition: all 2ms;
} }
.inline-icon{ .inline-icon, .header-icon{
vertical-align: middle; vertical-align: middle;
height: 1em; height: 1em;
} }
.header-icon{ .header-icon{
vertical-align: middle;
height: 1em;
padding: .5em; padding: .5em;
fill: var(--fg-deemph);
} }
a:hover, a:any-link{ a:hover, a:any-link{
text-decoration: none; text-decoration: none;
@@ -52,6 +82,14 @@ a:hover, a:any-link{
.table-link{ .table-link{
display: block; display: block;
width: 100%; width: 100%;
min-height: 1em;
height: 100%; height: 100%;
padding: 3px; padding: 1px;
}
.pagination{
display: flex;
justify-content: center;
}
.pagination a{
padding-left: 2em;
} }

11
src/epub2go_web/tasks.py Normal file
View File

@@ -0,0 +1,11 @@
from celery import shared_task
from django.conf import settings
from epub2go.convert import GBConvert
converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
@shared_task
def getEpub(book_url):
# TODO check for existing file and age
return converter.download(book_url)

View File

@@ -0,0 +1,61 @@
{% extends 'index.html' %}
{% load static %}
{% block main %}
<main>
<div class="search-container">
<search>
<form onsubmit="submitSearch(event)" class="search-bar">
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
</form>
</search>
<small>Im Moment finden sich hier <a href="{{ allbooks_url }}">{{ allbooks_count }} Bücher.</a> </small>
<a href="javascript:void(window.open('{{ http_host }}/?t=' + encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern">
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
</a>
<a href="https://github.com/eneller/epub2go-web" title="Projekt auf GitHub ansehen">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a>
</div>
<table id="table">
<caption>{{ page.paginator.count }} Ergebnisse gefunden:</caption>
<thead>
<tr>
<th></th>
<th>Titel (anklicken zum herunterladen)</th>
<th>Autor</th>
</tr>
</thead>
<tbody>
{% for item in page.object_list %}
<tr class="table-entry">
<td>
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
</a>
</td>
<td class="table-data">
<a href="./?t= {{ item.url }}" class="table-link">
{{ item.title }}
</a>
</td>
<td>
<a> {{ item.author }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if page.has_other_pages %}
<div class="pagination">
{% if page.has_previous %}
<a onclick="window.location.href = buildURI(1)" href="#"> &lt;&lt; </a>
<a onclick="window.location.href = buildURI('{{ page.previous_page_number }}')" href="#"> &lt; </a>
{% endif %}
{% if page.has_next %}
<a onclick="window.location.href = buildURI('{{ page.next_page_number }}')" href="#"> &gt; </a>
<a onclick="window.location.href = buildURI('{{ page.paginator.num_pages }}')" href="#"> &gt;&gt; </a>
{% endif %}
</div>
{% endif %}
</main>
{% endblock %}

View File

@@ -6,55 +6,20 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" > <meta name="viewport" content="width=device-width,initial-scale=1.0" >
<link rel="stylesheet" type="text/css" href="{% static 'styles.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'styles.css' %}">
<link rel="icon" type="image/png" href="{% static '/favicon/favicon-96x96.png' %}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{% static '/favicon/favicon.svg' %}" />
<link rel="shortcut icon" href="{% static '/favicon/favicon.ico' %}" />
<link rel="apple-touch-icon" sizes="180x180" href="{% static '/favicon/apple-touch-icon.png' %}" />
<meta name="apple-mobile-web-app-title" content="epub2go" />
<link rel="manifest" href="{% static 'site.webmanifest' %}" />
</head> </head>
<body> <body>
<header> <header>
<div> <h1><a href="{{ http_host }}">{{ title }}</a></h1>
<h1>{{ title }}</h1> <h2>1 Click Download für Literatur</h2>
<a href="javascript:void(window.open('http://{{ http_host }}/?t=' + encodeURIComponent(window.location.toString())))" title="Als Lesezeichen speichern"><!--TODO fix domain part as variable-->
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
</a>
<a href="https://github.com/eneller/epub2go-web">
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
</a></div>
<search>
<form onsubmit="submitSearch(event)" class="searchbar">
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
</form>
</search>
<small>Im Moment finden sich hier {{ book_count }} Bücher. </small>
</header> </header>
<main> {% block main %}
<!-- NOTE use dl here?--> {% endblock %}
<table id="table">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
{% for item in books %}
<tr>
<td>
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
</a>
</td>
<td class="table-data">
<a href="./?t= {{ item.url }}" class="table-link">
{{ item.title }}
</a>
</td>
<td>
<a> {{ item.author }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
<footer> <footer>
</footer> </footer>
</body> </body>

View File

@@ -0,0 +1,6 @@
{% extends 'index.html' %}
{% block main %}
<main>
<p class="main-banner">Einen Moment bitte. Deine Datei wird heruntergeladen.</p>
</main>
{% endblock %}

View File

@@ -15,7 +15,6 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@@ -23,6 +22,5 @@ from epub2go.convert import get_all_books, GBConvert, Book
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls),
path('',include('core.urls')) path('',include('core.urls'))
] ]

18
src/gunicorn.py Normal file
View File

@@ -0,0 +1,18 @@
# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "epub2go_web.wsgi"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 4
# The socket to bind
bind = "127.0.0.1:50000"
# Restart workers when code changes (development only!)
#reload = True
# Write access and error info to /var/log
#accesslog = errorlog = "/var/log/gunicorn/dev.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
#pidfile = "/var/run/gunicorn/dev.pid"
# Daemonize the Gunicorn process (detach & enter background)
#daemon = True

94
uv.lock generated
View File

@@ -46,7 +46,7 @@ wheels = [
[[package]] [[package]]
name = "celery" name = "celery"
version = "5.4.0" version = "5.5.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "billiard" }, { name = "billiard" },
@@ -56,12 +56,16 @@ dependencies = [
{ name = "click-repl" }, { name = "click-repl" },
{ name = "kombu" }, { name = "kombu" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "tzdata" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 } sdist = { url = "https://files.pythonhosted.org/packages/07/0c/0aaf310f4a2a048f010ebf63b8b0aaeb02ede2a79864fdbcdf31757a139d/celery-5.5.0.tar.gz", hash = "sha256:10d49f9926d16237310109b0e6e1e2f7a2133b84e684bb36534d7663e66919bb", size = 1665645 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 }, { url = "https://files.pythonhosted.org/packages/1a/98/d4eefe492ef810f304203cfff0f60554fab95514ec51b701a4529b66fa92/celery-5.5.0-py3-none-any.whl", hash = "sha256:f4170c6e5952281318448a899d9e9a15b9cbd007e002091766900dc8f71b9394", size = 438396 },
]
[package.optional-dependencies]
redis = [
{ name = "redis" },
] ]
[[package]] [[package]]
@@ -168,22 +172,22 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.7" version = "5.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "sqlparse" }, { name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/11186e493ddc5a5e92cc7924a6363f7d4c2b645f7d7cb04a26a63f9bfb8b/Django-5.1.7.tar.gz", hash = "sha256:30de4ee43a98e5d3da36a9002f287ff400b43ca51791920bfb35f6917bfe041c", size = 10716510 } sdist = { url = "https://files.pythonhosted.org/packages/4c/1b/c6da718c65228eb3a7ff7ba6a32d8e80fa840ca9057490504e099e4dd1ef/Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89", size = 10824891 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/0f/7e042df3d462d39ae01b27a09ee76653692442bc3701fbfa6cb38e12889d/Django-5.1.7-py3-none-any.whl", hash = "sha256:1323617cb624add820cb9611cdcc788312d250824f92ca6048fda8625514af2b", size = 8276912 }, { url = "https://files.pythonhosted.org/packages/63/e0/6a5b5ea350c5bd63fe94b05e4c146c18facb51229d9dee42aa39f9fc2214/Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83", size = 8301361 },
] ]
[[package]] [[package]]
name = "epub2go" name = "epub2go"
version = "2.1" version = "2.2.2"
source = { git = "https://github.com/eneller/epub2go.py#42677007633c1a4f7f87a2f8f02b9faa1715f037" } source = { git = "https://github.com/eneller/epub2go.py#b3cd49326f871ee6e73215ec2a8c393476a85f98" }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "click" }, { name = "click" },
@@ -198,16 +202,36 @@ name = "epub2go-py-web"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery", extra = ["redis"] },
{ name = "django" }, { name = "django" },
{ name = "epub2go" }, { name = "epub2go" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "pywatchman" },
{ name = "redis" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "celery", specifier = ">=5.4.0" }, { name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
{ name = "django", specifier = ">=5.1.6" }, { name = "django", specifier = ">=5.1.6" },
{ name = "epub2go", git = "https://github.com/eneller/epub2go.py" }, { name = "epub2go", git = "https://github.com/eneller/epub2go.py" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "pywatchman", specifier = ">=2.0.0" },
{ name = "redis", specifier = ">=5.2.1" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 },
] ]
[[package]] [[package]]
@@ -221,16 +245,25 @@ wheels = [
[[package]] [[package]]
name = "kombu" name = "kombu"
version = "5.5.0" version = "5.5.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "amqp" }, { name = "amqp" },
{ name = "tzdata" }, { name = "tzdata" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0f/a5/eddeaf1044b94d802ba9d69f5446f758a14b6770bcfd7498d5270fc2e68f/kombu-5.5.0.tar.gz", hash = "sha256:72e65c062e903ee1b4e8b68d348f63c02afc172eda409e3aca85867752e79c0b", size = 460768 } sdist = { url = "https://files.pythonhosted.org/packages/c8/12/7a340f48920f30d6febb65d0c4aca70ed01b29e116131152977df78a9a39/kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e", size = 461522 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/29/f4f109c490d3cab3cbbba53403895e6052c84a40f18c28f75cd4dd53f11d/kombu-5.5.0-py3-none-any.whl", hash = "sha256:526c6cf038c986b998639109a1eb762502f831e8da148cc928f1f95cd91eb874", size = 209313 }, { url = "https://files.pythonhosted.org/packages/af/ba/939f3db0fca87715c883e42cc93045347d61a9d519c270a38e54a06db6e1/kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd", size = 209763 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
] ]
[[package]] [[package]]
@@ -266,6 +299,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
] ]
[[package]]
name = "python-dotenv"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
name = "pywatchman"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/39/fc10dd952ac72a3a293936cd66a4551fdeb9012d2db99234a376100641ce/pywatchman-2.0.0.tar.gz", hash = "sha256:25354d9e3647f94411a4c13e510c83a1ceecc17977b0525ba41b16e7019c7b0c", size = 40570 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/7f/5d68d803489770cffa5d2b44be99b978c866f8a4d8e835f9da850415ed8a/pywatchman-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:51c2b4c72bea6b9fd90caf20759f5bc47febf0fd27bf2f247b87c66e2f6bab02", size = 52557 },
]
[[package]]
name = "redis"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@@ -322,11 +382,11 @@ wheels = [
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2025.1" version = "2025.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
] ]
[[package]] [[package]]