Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5ef95d69b | ||
|
|
c728f65e73 | ||
|
|
c1f813d70c | ||
|
|
aa66c06f88 | ||
|
|
518a8a1744 | ||
|
|
b65d878981 | ||
|
|
3d10081846 | ||
|
|
67b0654514 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
# 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.
|
||||||
@@ -8,6 +8,8 @@ dependencies = [
|
|||||||
"celery>=5.4.0",
|
"celery>=5.4.0",
|
||||||
"django>=5.1.6",
|
"django>=5.1.6",
|
||||||
"epub2go",
|
"epub2go",
|
||||||
|
"python-dotenv>=1.0.1",
|
||||||
|
"pywatchman>=2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
|
|||||||
4
src/.watchmanconfig
Normal file
4
src/.watchmanconfig
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"ignore_dirs": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
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.conf import settings
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import logging
|
|||||||
logger = logging.getLogger(__name__) #TODO configure logging
|
logger = logging.getLogger(__name__) #TODO configure logging
|
||||||
|
|
||||||
converter = GBConvert(downloaddir=settings.MEDIA_ROOT)
|
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
|
gbnetloc = urlparse(allbooks_url).netloc
|
||||||
|
|
||||||
def index(request: HttpRequest):
|
def index(request: HttpRequest):
|
||||||
@@ -21,22 +21,26 @@ def index(request: HttpRequest):
|
|||||||
'http_host': request.META['HTTP_HOST'],
|
'http_host': request.META['HTTP_HOST'],
|
||||||
'books': books,
|
'books': books,
|
||||||
'book_count': len(books),
|
'book_count': len(books),
|
||||||
|
'allbooks_url': allbooks_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
targetParam = request.GET.get('t', None)
|
targetParam = request.GET.get('t', None)
|
||||||
if validateUrl(targetParam):
|
if targetParam:
|
||||||
fpath = getEpub(targetParam)
|
if validateUrl(targetParam):
|
||||||
fname = os.path.basename(fpath)
|
# download file
|
||||||
file = open(fpath, 'rb')
|
fpath = getEpub(targetParam)
|
||||||
response = FileResponse(file)
|
fname = os.path.basename(fpath)
|
||||||
response['Content-Type'] = 'application/octet-stream'
|
file = open(fpath, 'rb')
|
||||||
response['Content-Disposition'] = f'attachment; filename="{fname}"'
|
response = FileResponse(file)
|
||||||
return response
|
response['Content-Type'] = 'application/octet-stream'
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{fname}"'
|
||||||
return render(request, 'index.html', context)
|
return response
|
||||||
|
else: return HttpResponseBadRequest('Input URL invalid.')
|
||||||
|
else:
|
||||||
|
# return base view
|
||||||
|
return render(request, 'index.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
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ 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
|
||||||
@@ -123,6 +123,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/"
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
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.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;
|
||||||
|
console.log(searchParam);
|
||||||
search(searchParam);
|
search(searchParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,9 +23,10 @@ function submitSearch(event){
|
|||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
function search(searchStr = searchInput.value){
|
function search(searchStr = searchInput.value){
|
||||||
|
searchStr= searchStr.toLowerCase();
|
||||||
function showMatch(tr){
|
function showMatch(tr){
|
||||||
// match search with list
|
// match search with list
|
||||||
let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent)
|
let searchSuccess = Array.from(tr.getElementsByClassName('table-data')).map(e => e.textContent.toLowerCase())
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.indexOf(searchStr) > -1;
|
.indexOf(searchStr) > -1;
|
||||||
if (searchSuccess) tr.style.display = "";
|
if (searchSuccess) tr.style.display = "";
|
||||||
|
|||||||
@@ -1,54 +1,78 @@
|
|||||||
/* 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{
|
||||||
|
background-color: var(--bg);
|
||||||
margin:40px auto;
|
margin:40px auto;
|
||||||
max-width:650px;
|
max-width:800px;
|
||||||
line-height:1.6;
|
line-height:1.4;
|
||||||
font-size:18px;
|
font-size:18px;
|
||||||
color:#444;
|
color:var(--fg);
|
||||||
padding:0
|
padding:0 10px;
|
||||||
10px}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
margin-bottom: 4%;
|
||||||
|
}
|
||||||
|
small{
|
||||||
|
color: var(--fg-deemph);
|
||||||
}
|
}
|
||||||
.searchbar{
|
.searchbar{
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
table, tr{
|
||||||
|
/* make table not resize when elements are hidden by searching */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
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;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
.table-link{
|
.table-link{
|
||||||
|
/* TODO fix links with no title/content being almost unclickable */
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 3px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
@@ -10,19 +10,21 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ title }}</h1>
|
<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-->
|
<h2>1 Click Download für Literatur</h2>
|
||||||
<img src="{% static 'bookmark.svg' %}" alt="Bookmarklet" class="header-icon">
|
</div>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/eneller/epub2go-web">
|
|
||||||
<img src="{% static 'github.svg' %}" alt="GitHub" class="header-icon">
|
|
||||||
</a></div>
|
|
||||||
<search>
|
<search>
|
||||||
<form onsubmit="submitSearch(event)" class="searchbar">
|
<form onsubmit="submitSearch(event)" class="searchbar">
|
||||||
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
|
<input type="search" id="searchInput" placeholder="Suche nach Titel" minlength="3">
|
||||||
</form>
|
</form>
|
||||||
</search>
|
</search>
|
||||||
<p>Im Moment finden sich hier {{ book_count }} Bücher. </p>
|
<small>Im Moment finden sich hier <a href="{{ allbooks_url }}">{{ book_count }} Bücher.</a> </small>
|
||||||
|
<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>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<!-- NOTE use dl here?-->
|
<!-- NOTE use dl here?-->
|
||||||
@@ -30,13 +32,13 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Title</th>
|
<th>Titel</th>
|
||||||
<th>Author</th>
|
<th>Autor</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in books %}
|
{% for item in books %}
|
||||||
<tr>
|
<tr class="table-entry">
|
||||||
<td>
|
<td>
|
||||||
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
|
<a href= {{ item.url }} target="_blank" rel="noopener noreferrer" class="table-link">
|
||||||
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
|
<img src="{% static 'open-link.svg' %}" alt="Open Link" class="inline-icon">
|
||||||
|
|||||||
22
uv.lock
generated
22
uv.lock
generated
@@ -201,6 +201,8 @@ dependencies = [
|
|||||||
{ name = "celery" },
|
{ name = "celery" },
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "epub2go" },
|
{ name = "epub2go" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pywatchman" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -208,6 +210,8 @@ requires-dist = [
|
|||||||
{ name = "celery", specifier = ">=5.4.0" },
|
{ name = "celery", 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 = "python-dotenv", specifier = ">=1.0.1" },
|
||||||
|
{ name = "pywatchman", specifier = ">=2.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -266,6 +270,24 @@ 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.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.3"
|
version = "2.32.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user