we've got a website, sorta!
This commit is contained in:
parent
7680a174fc
commit
6c94a346c4
56
dlibrary.py
56
dlibrary.py
|
@ -5,12 +5,14 @@ import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from os.path import relpath, splitext
|
from os.path import relpath, splitext
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from dlsite_async import DlsiteAPI
|
from dlsite_async import DlsiteAPI
|
||||||
import fitz
|
import fitz
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
NUMBER_REGEX = re.compile('[0-9]+')
|
NUMBER_REGEX = re.compile('[0-9]+')
|
||||||
|
@ -159,7 +161,7 @@ def collate(args):
|
||||||
for work_path in extraction_dir.iterdir():
|
for work_path in extraction_dir.iterdir():
|
||||||
work_id = work_path.name
|
work_id = work_path.name
|
||||||
|
|
||||||
collation_dir = args.destdir / 'site' / 'works' / work_id
|
collation_dir = args.destdir / 'site' / 'images' / work_id
|
||||||
if collation_dir.exists():
|
if collation_dir.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -200,7 +202,7 @@ def collate(args):
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
def manual_collate(args):
|
def manual_collate(args):
|
||||||
collation_dir = args.destdir / 'site' / 'works' / args.work_id
|
collation_dir = args.destdir / 'site' / 'images' / args.work_id
|
||||||
if collation_dir.exists() and len(list(collation_dir.iterdir())) > 0:
|
if collation_dir.exists() and len(list(collation_dir.iterdir())) > 0:
|
||||||
print(f'Collation directory already exists!')
|
print(f'Collation directory already exists!')
|
||||||
return
|
return
|
||||||
|
@ -256,7 +258,55 @@ def metadata(args):
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
def publish(args):
|
def publish(args):
|
||||||
pass
|
source_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
jenv = Environment(
|
||||||
|
loader=FileSystemLoader(source_dir / "templates"),
|
||||||
|
autoescape=select_autoescape()
|
||||||
|
)
|
||||||
|
|
||||||
|
viewer_template = jenv.get_template("viewer.html")
|
||||||
|
|
||||||
|
con = sqlite3.connect(args.destdir / 'meta.db')
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
collated_work_ids = {p.name for p in (args.destdir / 'site' / 'images').iterdir()}
|
||||||
|
|
||||||
|
works = []
|
||||||
|
for (work_id, title, circle, date, description, series) in cur.execute('SELECT id, title, circle, date, description, series FROM works ORDER BY date DESC').fetchall():
|
||||||
|
if work_id not in collated_work_ids:
|
||||||
|
continue
|
||||||
|
authors = [author for (author,) in cur.execute('SELECT author FROM authors WHERE work = ?', (work_id,))]
|
||||||
|
tags = [tag for (tag,) in cur.execute('SELECT tag FROM tags WHERE work = ?', (work_id,))]
|
||||||
|
work = {
|
||||||
|
'id': work_id,
|
||||||
|
'title': title,
|
||||||
|
'circle': circle,
|
||||||
|
'date': date,
|
||||||
|
'description': description,
|
||||||
|
'series': series,
|
||||||
|
'authors': authors,
|
||||||
|
'tags': tags,
|
||||||
|
}
|
||||||
|
works.append(work)
|
||||||
|
|
||||||
|
images = [path.name for path in (args.destdir / 'site' / 'images' / work_id).iterdir()]
|
||||||
|
images.sort()
|
||||||
|
|
||||||
|
work_dir = args.destdir / 'site' / 'works' / work_id
|
||||||
|
work_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(work_dir / 'index.html', 'w') as f:
|
||||||
|
f.write(viewer_template.render(depth=2, work=work, title=title, images=images))
|
||||||
|
|
||||||
|
shutil.copytree(source_dir / 'static', args.destdir / 'site' / 'static', dirs_exist_ok=True)
|
||||||
|
|
||||||
|
list_template = jenv.get_template("list.html")
|
||||||
|
|
||||||
|
with open(args.destdir / 'site' / 'index.html', 'w') as f:
|
||||||
|
f.write(list_template.render(depth=0, works=works))
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser(prog='dlibrary')
|
argparser = argparse.ArgumentParser(prog='dlibrary')
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
pymupdf
|
pymupdf
|
||||||
requests
|
requests
|
||||||
dlsite-async
|
dlsite-async
|
||||||
|
jinja2
|
||||||
];
|
];
|
||||||
src = ./.;
|
src = ./.;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
requests
|
requests
|
||||||
PyMuPDF
|
PyMuPDF
|
||||||
dlsite-async
|
dlsite-async
|
||||||
|
jinja2
|
||||||
|
|
76
static/dlibrary.css
Normal file
76
static/dlibrary.css
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
body {
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* listing stuff */
|
||||||
|
|
||||||
|
#list-title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-listing {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #333;
|
||||||
|
padding: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 360px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* viewer stuff */
|
||||||
|
|
||||||
|
#viewer-images {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-num, #duration {
|
||||||
|
position: fixed;
|
||||||
|
font-size: 14pt;
|
||||||
|
top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.75;
|
||||||
|
text-shadow: /* Duplicate the same shadow to make it very strong */
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-num {
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#duration {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
background-color: #4488ffcc;
|
||||||
|
height: 5px;
|
||||||
|
width: 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
6
static/viewer.css
Normal file
6
static/viewer.css
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
120
static/viewer.js
Normal file
120
static/viewer.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const pages = Array.from(document.querySelectorAll('img.viewer-image'));
|
||||||
|
let currentPage = parseInt(localStorage.getItem(`${WORK_ID}-currentPage`)) || 0;
|
||||||
|
let duration = parseInt(localStorage.getItem(`${WORK_ID}-duration`)) || 10;
|
||||||
|
let paused = true;
|
||||||
|
let interval;
|
||||||
|
let elapsed = 0;
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
interval = setInterval(
|
||||||
|
function () {
|
||||||
|
if (paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elapsed += 100;
|
||||||
|
if (elapsed >= duration*1000) {
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
updateBar();
|
||||||
|
},
|
||||||
|
100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressBar = document.getElementById('progress');
|
||||||
|
function updateBar() {
|
||||||
|
progressBar.style.width = `${100*elapsed/(1000*duration)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
elapsed = 0;
|
||||||
|
updateBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(pageNum) {
|
||||||
|
elapsed = 0;
|
||||||
|
|
||||||
|
const previous = pages[currentPage];
|
||||||
|
const current = pages[pageNum];
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous.classList.remove('current');
|
||||||
|
current.classList.add('current');
|
||||||
|
|
||||||
|
currentPage = pageNum;
|
||||||
|
localStorage.setItem(`${WORK_ID}-currentPage`, currentPage);
|
||||||
|
|
||||||
|
const display = document.getElementById('image-container');
|
||||||
|
display.style.backgroundImage = `url("${current.src}")`;
|
||||||
|
|
||||||
|
document.getElementById('page-num')
|
||||||
|
.innerText = [
|
||||||
|
(pageNum + 1).toLocaleString(),
|
||||||
|
pages.length.toLocaleString()
|
||||||
|
].join('\u200a/\u200a');
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDuration(secs, pause) {
|
||||||
|
duration = secs;
|
||||||
|
localStorage.setItem(`${WORK_ID}-duration`, duration);
|
||||||
|
paused = pause;
|
||||||
|
|
||||||
|
document.getElementById('duration').textContent = (paused ? '[paused] ' : '') + duration.toLocaleString() + 's';
|
||||||
|
if (paused) {
|
||||||
|
stopTimer();
|
||||||
|
} else {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(currentPage);
|
||||||
|
changeDuration(duration, paused);
|
||||||
|
|
||||||
|
document.onkeydown = event =>{
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 32: //space
|
||||||
|
changeDuration(duration, !paused);
|
||||||
|
break;
|
||||||
|
case 37: //left
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 38: //up
|
||||||
|
if (2 <= duration && duration <= 10) {
|
||||||
|
changeDuration(duration - 1, false);
|
||||||
|
} else if (10 < duration && duration <= 20) {
|
||||||
|
changeDuration(duration - 2.5, false);
|
||||||
|
} else if (20 < duration) {
|
||||||
|
changeDuration(duration - 5, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 39: //right
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
case 40: //down
|
||||||
|
if (duration < 10) {
|
||||||
|
changeDuration(duration + 1, false);
|
||||||
|
} else if (10 <= duration && duration < 20) {
|
||||||
|
changeDuration(duration + 2.5, false);
|
||||||
|
} else if (20 <= duration) {
|
||||||
|
changeDuration(duration + 5, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 13: //enter
|
||||||
|
changeDuration(duration, true);
|
||||||
|
localStorage.setItem(`${WORK_ID}-currentPage`, 0);
|
||||||
|
window.location.href = ROOT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
15
templates/base.html
Normal file
15
templates/base.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% from 'utils.html' import root with context -%}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="dark">
|
||||||
|
<title>{% if title %}{{ title }} - {% else %}{% endif %}DLibrary</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ root() }}static/dlibrary.css">
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body required %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
20
templates/list.html
Normal file
20
templates/list.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block body %}
|
||||||
|
{% from 'utils.html' import root with context %}
|
||||||
|
<h1 id="list-title"><a href={{ root() }}>DLibrary</a> {% block list_title %}{% endblock %}</h1>
|
||||||
|
<div class="card-listing">
|
||||||
|
{% for work in works %}
|
||||||
|
<div class="card">
|
||||||
|
<a href="{{ root() }}works/{{ work['id'] }}/">
|
||||||
|
<img src="{{ root() }}thumbnails/{{ work['id'] }}.jpg">
|
||||||
|
<div class="card-authors">
|
||||||
|
[{% if work['circle'] %}{{ work['circle'] }}{% endif %}{% if work['circle'] and work['authors'] %} ({% endif %}{{ ', '.join(work['authors']) }}{% if work['circle'] and work['authors'] %}){% endif %}]
|
||||||
|
</div>
|
||||||
|
<div class="card-title">
|
||||||
|
{{ work['title'] }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
1
templates/utils.html
Normal file
1
templates/utils.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{% macro root() %}{% for i in range(depth) %}../{% endfor %}{% endmacro %}
|
21
templates/viewer.html
Normal file
21
templates/viewer.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from 'utils.html' import root with context %}
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ root() }}static/viewer.css">
|
||||||
|
<script>
|
||||||
|
const WORK_ID = "{{ work['id'] }}";
|
||||||
|
const ROOT = "{{ root() }}";
|
||||||
|
</script>
|
||||||
|
<script src="{{ root() }}static/viewer.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div id="viewer-images">
|
||||||
|
{% for filename in images %}
|
||||||
|
<img src="{{ root() }}images/{{ work['id'] }}/{{ filename }}" class="viewer-image">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div id="progress"></div>
|
||||||
|
<div id="page-num"></div>
|
||||||
|
<div id="duration"></div>
|
||||||
|
<div id="image-container"></div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue