32 Commits
v0.1 ... v2.1

Author SHA1 Message Date
eneller
d32e6f2ddb feat: case-insensitive search 2025-04-13 12:43:31 +02:00
eneller
b32f4465ef build: upgrade uv.lock 2025-04-06 10:30:36 +02:00
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
eneller
67b0654514 fix: bookmarklet 2025-03-19 19:34:53 +01:00
27 changed files with 443 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
staticfiles/

View File

@@ -1,2 +1,35 @@
# 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

@@ -1,13 +1,17 @@
[project]
name = "epub2go-py-web"
version = "0.1.0"
version = "2.1"
description = "Web Interface to epub2go.py"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"celery>=5.4.0",
"celery[redis]>=5.4.0",
"django>=5.1.6",
"epub2go",
"gunicorn>=23.0.0",
"python-dotenv>=1.0.1",
"pywatchman>=2.0.0",
"redis>=5.2.1",
]
[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.http import HttpRequest, HttpResponse, FileResponse
from django.conf import settings
from celery import shared_task
from django.http import HttpRequest, HttpResponse, FileResponse, HttpResponseBadRequest
from django.core.paginator import Paginator
from epub2go.convert import get_all_books, Book, GBConvert, allbooks_url
from epub2go.convert import get_all_books, Book, allbooks_url
import os
from urllib.parse import urlparse
import logging
from epub2go_web.tasks import getEpub
logger = logging.getLogger(__name__) #TODO configure logging
converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
books = get_all_books()# TODO get from pickle
books = sorted(get_all_books(), key= lambda b: b.title)# TODO get from pickle
gbnetloc = urlparse(allbooks_url).netloc
def index(request: HttpRequest):
context = {
'title': 'epub2go',
'http_host': request.META['HTTP_HOST'],
'books': books,
'book_count': len(books),
}
localbooks = books
targetParam = request.GET.get('t', None)
if validateUrl(targetParam):
fpath = getEpub(targetParam)
fname = os.path.basename(fpath)
file = open(fpath, 'rb')
response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = f'attachment; filename="{fname}"'
return response
searchParam = request.GET.get('s', None)
if targetParam:
if validateUrl(targetParam):
# download file
result = getEpub.delay(targetParam)
fpath = result.get(timeout=60)
fname = os.path.basename(fpath)
file = open(fpath, 'rb')
response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = f'attachment; filename="{fname}"'
return response
else: return HttpResponseBadRequest('Input URL invalid.')
elif searchParam:
localbooks = [book for book in books if searchParam.lower() in book.title.lower()]
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 :
if not param: return False
netloc = urlparse(param).netloc
if(netloc == gbnetloc): return True
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
# 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"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
@@ -46,7 +44,6 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
@@ -64,7 +61,6 @@ TEMPLATES = [
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
@@ -73,48 +69,18 @@ TEMPLATES = [
WSGI_APPLICATION = "epub2go_web.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
#DATABASES = {
# "default": {
# "ENGINE": "django.db.backends.sqlite3",
# "NAME": BASE_DIR / "db.sqlite3",
# }
#}
# 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
# Celery settings
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
CELERY_TASK_ALWAYS_EAGER = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
@@ -123,11 +89,7 @@ STATIC_URL = "static/"
STATICFILES_DIRS = [
PROJ_DIR / "static/",
]
STATIC_ROOT = PROJ_DIR/ "staticfiles"
MEDIA_URL = "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 searchInput = document.getElementById('searchInput');
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');
if (searchParam){
searchInput.value = searchParam;
search(searchParam);
}
function submitSearch(event){
event.preventDefault();
search();
}
function search(searchStr = searchInput.value){
function showMatch(tr){
// match search with list
let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent)
.join(' ')
.indexOf(searchStr) > -1;
if (searchSuccess) tr.style.display = "";
else tr.style.display = "none";
}
table_r.map(showMatch, table_r);
let path = window.location.pathname + "./?s="+encodeURIComponent(searchInput.value);
window.location.href= path;
}
function buildURI(p){
let params = new URLSearchParams(window.location.search);
params.set('p', p);
return `${window.location.pathname}?${params.toString()}`;
}

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,46 +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{
margin:40px auto;
max-width:650px;
line-height:1.6;
background-color: var(--bg);
overflow-x: hidden;
margin:.5em auto;
max-width:min(100%,800px);
line-height:1.4;
font-size:18px;
color:#444;
padding:0
10px}
color:var(--fg);
padding:0 .2em;
justify-content: center;
}
h1,h2,h3{
line-height:1.2
}
/* custom styles here */
:root{
--white:#faf0e673;
}
body{
background-color: var(--white)
line-height:1.2;
letter-spacing: -2%;
}
header{
text-align: center;
justify-content: center;
}
.searchbar{
width: fit-content;
small{
color: var(--fg-deemph);
}
.search-bar{
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){
background-color: #EEEEEE;
background-color: var(--bg-acc);
}
tr:hover{
background-color: #DDDDDD;
background-color: var(--bg-hover);
transition: all 2ms;
}
.inline-icon{
.inline-icon, .header-icon{
vertical-align: middle;
height: 1em;
}
.header-icon{
vertical-align: middle;
height: 1em;
padding: .5em;
fill: var(--fg-deemph);
}
a:hover, a:any-link{
text-decoration: none;
@@ -49,6 +82,14 @@ a:hover, a:any-link{
.table-link{
display: block;
width: 100%;
min-height: 1em;
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

@@ -2,59 +2,24 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" >
<link rel="stylesheet" type="text/css" href="{% static 'styles.css' %}">
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0" >
<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>
<body>
<header>
<div>
<h1>{{ title }}</h1>
<a href="javascript:void(window.open('{{ 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>
<p>Im Moment finden sich hier {{ book_count }} Bücher. </p>
<h1><a href="{{ http_host }}">{{ title }}</a></h1>
<h2>1 Click Download für Literatur</h2>
</header>
<main>
<!-- NOTE use dl here?-->
<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>
{% block main %}
{% endblock %}
<footer>
</footer>
</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'))
"""
from django.contrib import admin
from django.urls import path, include
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
@@ -23,6 +22,5 @@ from epub2go.convert import get_all_books, GBConvert, Book
urlpatterns = [
path("admin/", admin.site.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]]
name = "celery"
version = "5.4.0"
version = "5.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "billiard" },
@@ -56,12 +56,16 @@ dependencies = [
{ name = "click-repl" },
{ name = "kombu" },
{ name = "python-dateutil" },
{ name = "tzdata" },
{ 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 = [
{ 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]]
@@ -168,22 +172,22 @@ wheels = [
[[package]]
name = "django"
version = "5.1.7"
version = "5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ 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 = [
{ 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]]
name = "epub2go"
version = "2.1"
source = { git = "https://github.com/eneller/epub2go.py#42677007633c1a4f7f87a2f8f02b9faa1715f037" }
version = "2.2.3"
source = { git = "https://github.com/eneller/epub2go.py#75974ae11957b8a65d607b1f00ad2fae41a6840b" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "click" },
@@ -198,16 +202,36 @@ name = "epub2go-py-web"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "celery" },
{ name = "celery", extra = ["redis"] },
{ name = "django" },
{ name = "epub2go" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "pywatchman" },
{ name = "redis" },
]
[package.metadata]
requires-dist = [
{ name = "celery", specifier = ">=5.4.0" },
{ name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
{ name = "django", specifier = ">=5.1.6" },
{ 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]]
@@ -221,16 +245,25 @@ wheels = [
[[package]]
name = "kombu"
version = "5.5.0"
version = "5.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "amqp" },
{ name = "tzdata" },
{ 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 = [
{ 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]]
@@ -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 },
]
[[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]]
name = "requests"
version = "2.32.3"
@@ -322,11 +382,11 @@ wheels = [
[[package]]
name = "tzdata"
version = "2025.1"
version = "2025.2"
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 = [
{ 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]]