Compare commits
10 commits
3ef82c8024
...
b437fcc79e
Author | SHA1 | Date | |
---|---|---|---|
xenofem | b437fcc79e | ||
xenofem | a825162dee | ||
xenofem | 79b946889d | ||
xenofem | 323ce158f9 | ||
xenofem | 528ad4e6f2 | ||
xenofem | ecb63ced83 | ||
xenofem | 1f15abed9a | ||
xenofem | fb7d275ebb | ||
xenofem | e3eeded952 | ||
xenofem | b5412d5d33 |
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2024 xenofem
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -4,10 +4,12 @@ import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import importlib_resources as resources
|
import importlib_resources as resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from os import getenv
|
||||||
from os.path import relpath, splitext
|
from os.path import relpath, splitext
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import textwrap
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
@ -296,12 +298,15 @@ def metadata(args):
|
||||||
|
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
def copy_contents(src, dest):
|
def copy_recursive(src, dest):
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
for item in src.iterdir():
|
for item in src.iterdir():
|
||||||
|
if item.is_dir() and not item.is_symlink():
|
||||||
|
copy_recursive(item, dest / item.name)
|
||||||
|
else:
|
||||||
shutil.copyfile(item, dest / item.name)
|
shutil.copyfile(item, dest / item.name)
|
||||||
|
|
||||||
def publish(args):
|
def generate(args):
|
||||||
jenv = Environment(
|
jenv = Environment(
|
||||||
loader=PackageLoader("dlibrary"),
|
loader=PackageLoader("dlibrary"),
|
||||||
autoescape=select_autoescape()
|
autoescape=select_autoescape()
|
||||||
|
@ -347,7 +352,7 @@ def publish(args):
|
||||||
with open(viewer_dir / 'index.html', 'w') as f:
|
with open(viewer_dir / 'index.html', 'w') as f:
|
||||||
f.write(viewer_template.render(depth=3, work=work, title=title, images=images))
|
f.write(viewer_template.render(depth=3, work=work, title=title, images=images))
|
||||||
|
|
||||||
def make_categorization(categorization, query, work_filter):
|
def make_categorization(categorization, query, work_filter, work_style_cards=False):
|
||||||
categorization_dir = args.destdir / 'site' / categorization
|
categorization_dir = args.destdir / 'site' / categorization
|
||||||
|
|
||||||
cats = [cat for (cat,) in cur.execute(query)]
|
cats = [cat for (cat,) in cur.execute(query)]
|
||||||
|
@ -374,6 +379,7 @@ def publish(args):
|
||||||
categorization=categorization,
|
categorization=categorization,
|
||||||
categories=cats,
|
categories=cats,
|
||||||
samples=cat_samples,
|
samples=cat_samples,
|
||||||
|
work_style_cards=work_style_cards,
|
||||||
))
|
))
|
||||||
|
|
||||||
make_categorization(
|
make_categorization(
|
||||||
|
@ -395,10 +401,11 @@ def publish(args):
|
||||||
'series',
|
'series',
|
||||||
'SELECT DISTINCT series FROM works WHERE series NOT NULL ORDER BY series',
|
'SELECT DISTINCT series FROM works WHERE series NOT NULL ORDER BY series',
|
||||||
lambda series: lambda work: work['series'] == series,
|
lambda series: lambda work: work['series'] == series,
|
||||||
|
work_style_cards=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with resources.as_file(resources.files("dlibrary")) as r:
|
with resources.as_file(resources.files("dlibrary")) as r:
|
||||||
copy_contents(r / 'static', args.destdir / 'site' / 'static')
|
copy_recursive(r / 'static', args.destdir / 'site' / 'static')
|
||||||
|
|
||||||
with open(args.destdir / 'site' / 'index.html', 'w') as f:
|
with open(args.destdir / 'site' / 'index.html', 'w') as f:
|
||||||
f.write(list_template.render(depth=0, works=works))
|
f.write(list_template.render(depth=0, works=works))
|
||||||
|
@ -406,12 +413,33 @@ def publish(args):
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser(prog='dlibrary')
|
argparser = argparse.ArgumentParser(
|
||||||
|
prog='dlibrary',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
description=textwrap.dedent("""\
|
||||||
|
Organize DRM-free works purchased from DLSite into a library
|
||||||
|
that can be viewed in a web browser.
|
||||||
|
|
||||||
|
Intended workflow:
|
||||||
|
- `extract` a collection of zipfiles downloaded from DLSite
|
||||||
|
into DLibrary's data directory, giving each work its own
|
||||||
|
subfolder.
|
||||||
|
- `fetch` metadata and thumbnail images for extracted works
|
||||||
|
from DLSite.
|
||||||
|
- `collate` and/or `manual-collate` extracted works,
|
||||||
|
producing a single sequence of image files (or symlinks
|
||||||
|
into the extracted data, when possible) for each work.
|
||||||
|
- Manually adjust works' `metadata` when necessary.
|
||||||
|
- `generate` a static website providing a catalog and viewer
|
||||||
|
for all collated works.
|
||||||
|
"""),
|
||||||
|
)
|
||||||
|
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'-d', '--destdir',
|
'-d', '--destdir',
|
||||||
type=Path,
|
type=Path,
|
||||||
default=Path('./dlibrary'),
|
default=Path(getenv('DLIBRARY_DIR', './dlibrary')),
|
||||||
help='directory to store dlibrary content and metadata to (default: ./dlibrary)',
|
help='directory to store dlibrary content and metadata to (default: $DLIBRARY_DIR or ./dlibrary)',
|
||||||
)
|
)
|
||||||
subparsers = argparser.add_subparsers(title="subcommands", required=True)
|
subparsers = argparser.add_subparsers(title="subcommands", required=True)
|
||||||
|
|
||||||
|
@ -433,23 +461,79 @@ parser_extract.set_defaults(func=extract)
|
||||||
parser_fetch = subparsers.add_parser('fetch', help='fetch metadata and thumbnails')
|
parser_fetch = subparsers.add_parser('fetch', help='fetch metadata and thumbnails')
|
||||||
parser_fetch.set_defaults(func=fetch)
|
parser_fetch.set_defaults(func=fetch)
|
||||||
|
|
||||||
parser_collate = subparsers.add_parser('collate', help='collate a single sequence of image files for each work')
|
parser_collate = subparsers.add_parser(
|
||||||
|
'collate',
|
||||||
|
help='collate each work into a sequence of image files',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
description=textwrap.dedent("""\
|
||||||
|
For each extracted work that has not already been collated,
|
||||||
|
DLibrary will attempt to intuit its structure as follows:
|
||||||
|
|
||||||
|
- Enter the work's directory. If the directory contains
|
||||||
|
nothing except a single subdirectory (ignoring a few types
|
||||||
|
of files that are definitely not relevant), traverse
|
||||||
|
downwards repeatedly.
|
||||||
|
- If the current directory contains nothing except a single
|
||||||
|
PDF (again, ignoring irrelevant files), attempt to extract
|
||||||
|
a series of images from the PDF. This process expects that
|
||||||
|
each page of the PDF consists of a single embedded image,
|
||||||
|
which will be extracted at full resolution. Support for
|
||||||
|
more complex PDFs is not yet implemented.
|
||||||
|
- If the current directory contains nothing except image
|
||||||
|
files, and the image files are named in a way that clearly
|
||||||
|
indicates a complete numerical order (each filename
|
||||||
|
consists of a shared prefix followed by a distinct
|
||||||
|
number), symlink files in the inferred order.
|
||||||
|
- Otherwise, skip processing this work for now.
|
||||||
|
|
||||||
|
DLibrary can be given "collation hints" which provide
|
||||||
|
alternative starting points for this search process. A hint
|
||||||
|
is a path under $DLIBRARY_DIR/extract/[work id]/
|
||||||
|
indicating a different directory or PDF file to begin the
|
||||||
|
search process for that work, rather than starting at the
|
||||||
|
top level of the extracted data. There can be at most one
|
||||||
|
hint per work; for more complicated scenarios where a work
|
||||||
|
includes multiple folders that need to be collated together,
|
||||||
|
or where filenames do not clearly indicate an ordering, use
|
||||||
|
`manual-collate` instead.
|
||||||
|
"""),
|
||||||
|
)
|
||||||
parser_collate.add_argument(
|
parser_collate.add_argument(
|
||||||
'hints',
|
'hints',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
type=Path,
|
type=Path,
|
||||||
nargs='*',
|
nargs='*',
|
||||||
help='manually-specified paths of subdirectories or PDFs within extraction folders, at most one per work',
|
help='paths within extraction folders as collation hints'
|
||||||
)
|
)
|
||||||
parser_collate.set_defaults(func=collate)
|
parser_collate.set_defaults(func=collate)
|
||||||
|
|
||||||
parser_manual_collate = subparsers.add_parser('manual-collate', help='collate a specific work manually, specifying all paths to include')
|
parser_manual_collate = subparsers.add_parser(
|
||||||
|
'manual-collate',
|
||||||
|
help='collate a single work manually',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
description=textwrap.dedent("""\
|
||||||
|
All provided paths must be under $DLIBRARY_DIR/extract/[work id]/
|
||||||
|
for the work being manually collated. `manual-collate` can
|
||||||
|
only handle one work at a time. Paths are used as follows:
|
||||||
|
|
||||||
|
- If a path is a directory, all *image files* immediately
|
||||||
|
inside that directory will be appended to the sequence. If
|
||||||
|
files are named in a way which indicates a clear ordering,
|
||||||
|
that ordering will be used. Otherwise, filenames will be
|
||||||
|
sorted lexicographically. Non-image files and
|
||||||
|
subdirectories will be ignored.
|
||||||
|
- If a path is an image file, that image file will be
|
||||||
|
appended to the sequence.
|
||||||
|
- If a path is a PDF file, page images will be extracted
|
||||||
|
from that PDF and appended to the sequence.
|
||||||
|
"""),
|
||||||
|
)
|
||||||
parser_manual_collate.add_argument(
|
parser_manual_collate.add_argument(
|
||||||
'paths',
|
'paths',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
type=Path,
|
type=Path,
|
||||||
nargs='+',
|
nargs='+',
|
||||||
help='paths of files (images to symlink, pdfs to extract) or directories (symlink all images in the directory, no recursion, best-effort sorting)'
|
help='paths within a single work to be collated in sequence',
|
||||||
)
|
)
|
||||||
parser_manual_collate.set_defaults(func=manual_collate)
|
parser_manual_collate.set_defaults(func=manual_collate)
|
||||||
|
|
||||||
|
@ -462,8 +546,20 @@ parser_metadata.add_argument(
|
||||||
)
|
)
|
||||||
parser_metadata.set_defaults(func=metadata)
|
parser_metadata.set_defaults(func=metadata)
|
||||||
|
|
||||||
parser_publish = subparsers.add_parser('publish', help='generate HTML/CSS/JS for library site')
|
parser_generate = subparsers.add_parser(
|
||||||
parser_publish.set_defaults(func=publish)
|
'generate',
|
||||||
|
help='generate HTML/CSS/JS for library site',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
description=textwrap.dedent("""\
|
||||||
|
The static site will be generated under $DLIBRARY_DIR/site/
|
||||||
|
and can be served by pointing an HTTP server at that
|
||||||
|
directory. Note that some files inside the static site
|
||||||
|
hierarchy will be symlinks into $DLIBRARY_DIR/extract/
|
||||||
|
outside the site hierarchy, so make sure your HTTP server
|
||||||
|
will allow those symlinks to be read.
|
||||||
|
"""),
|
||||||
|
)
|
||||||
|
parser_generate.set_defaults(func=generate)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
|
|
|
@ -51,12 +51,19 @@ body {
|
||||||
|
|
||||||
.work-container {
|
.work-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.work-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.work-info {
|
.work-info {
|
||||||
max-width: min(50%, 500px);
|
flex-basis: 40%;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-info td, .work-info th {
|
.work-info td, .work-info th {
|
||||||
|
|
|
@ -4,3 +4,39 @@ html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tap {
|
||||||
|
background: #000000;
|
||||||
|
opacity: 0.8;
|
||||||
|
position: fixed;
|
||||||
|
animation: 2s linear forwards fade;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#tap-left, #tap-right {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 30vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tap-left {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tap-right {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tap-back {
|
||||||
|
top: 0px;
|
||||||
|
left: 30vw;
|
||||||
|
width: 40vw;
|
||||||
|
height: 20vh;
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
let paused = true;
|
let paused = true;
|
||||||
let interval;
|
let interval;
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
|
let rtl = (localStorage.getItem(`${WORK_ID}-rtl`) !== "false");
|
||||||
|
|
||||||
function startTimer() {
|
function startTimer() {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
|
@ -78,16 +79,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function left() {
|
||||||
|
if (currentPage === 0) {
|
||||||
|
rtl = true;
|
||||||
|
localStorage.setItem(`${WORK_ID}-rtl`, rtl);
|
||||||
|
}
|
||||||
|
changePage(currentPage + (rtl ? 1 : -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function right() {
|
||||||
|
if (currentPage === 0) {
|
||||||
|
rtl = false;
|
||||||
|
localStorage.setItem(`${WORK_ID}-rtl`, rtl);
|
||||||
|
}
|
||||||
|
changePage(currentPage + (rtl ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitToWork() {
|
||||||
|
changeDuration(duration, true);
|
||||||
|
localStorage.setItem(`${WORK_ID}-currentPage`, 0);
|
||||||
|
window.location.href = `../${INDEX}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTapZones() {
|
||||||
|
for (const el of document.getElementsByClassName('tap')) {
|
||||||
|
el.style.opacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changePage(currentPage);
|
changePage(currentPage);
|
||||||
changeDuration(duration, paused);
|
changeDuration(duration, paused);
|
||||||
|
|
||||||
document.onkeydown = event =>{
|
document.onkeydown = event => {
|
||||||
|
hideTapZones();
|
||||||
|
|
||||||
switch (event.keyCode) {
|
switch (event.keyCode) {
|
||||||
case 32: //space
|
case 32: //space
|
||||||
changeDuration(duration, !paused);
|
changeDuration(duration, !paused);
|
||||||
break;
|
break;
|
||||||
case 37: //left
|
case 37: //left
|
||||||
changePage(currentPage - 1);
|
left();
|
||||||
break;
|
break;
|
||||||
case 38: //up
|
case 38: //up
|
||||||
if (2 <= duration && duration <= 10) {
|
if (2 <= duration && duration <= 10) {
|
||||||
|
@ -99,7 +130,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 39: //right
|
case 39: //right
|
||||||
changePage(currentPage + 1);
|
right();
|
||||||
break;
|
break;
|
||||||
case 40: //down
|
case 40: //down
|
||||||
if (duration < 10) {
|
if (duration < 10) {
|
||||||
|
@ -111,10 +142,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 13: //enter
|
case 13: //enter
|
||||||
changeDuration(duration, true);
|
exitToWork();
|
||||||
localStorage.setItem(`${WORK_ID}-currentPage`, 0);
|
|
||||||
window.location.href = "../";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.onclick = hideTapZones;
|
||||||
|
document.getElementById("tap-left").onclick = left;
|
||||||
|
document.getElementById("tap-right").onclick = right;
|
||||||
|
document.getElementById("tap-back").onclick = exitToWork;
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
<title>{% block title %}{% if title %}{{ title }} - {% else %}{% endif %}DLibrary{% endblock %}</title>
|
<title>{% block title %}{% if title %}{{ title }} - {% else %}{% endif %}DLibrary{% endblock %}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ root() }}static/dlibrary.css">
|
<link rel="stylesheet" type="text/css" href="{{ root() }}/static/dlibrary.css">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ categorization.capitalize() }} - DLibrary{% endblock %}
|
{% block title %}{{ categorization.capitalize() }} - DLibrary{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from 'utils.html' import urlcat, root with context %}
|
{% from 'utils.html' import urlcat, index, root with context %}
|
||||||
<h1 id="title"><a href="{{ root() }}">DLibrary</a> > <a href="{{ root() }}{{ categorization }}">{{ categorization.capitalize() }}</a></h1>
|
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a> > {{ categorization.capitalize() }}</h1>
|
||||||
{% include 'nav.html' %}
|
{% include 'nav.html' %}
|
||||||
<div class="card-listing">
|
<div class="card-listing">
|
||||||
{% for cat in categories %}
|
{% for cat in categories %}
|
||||||
<div class="card category">
|
<div class="card {% if not work_style_cards %}category{% endif %}">
|
||||||
<a href="{{ root() }}{{ categorization }}/{{ urlcat(cat) }}/">
|
<a href="{{ root() }}/{{ categorization }}/{{ urlcat(cat) }}/{{ index() }}">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
{{ cat }}
|
{{ cat }}
|
||||||
</div>
|
</div>
|
||||||
{% if samples[cat] %}
|
{% if samples[cat] %}
|
||||||
<img src="{{ root() }}thumbnails/{{ samples[cat]['id'] }}.jpg">
|
<img src="{{ root() }}/thumbnails/{{ samples[cat]['id'] }}.jpg">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from 'utils.html' import root with context %}
|
{% from 'utils.html' import index, root with context %}
|
||||||
<h1 id="title"><a href="{{ root() }}">DLibrary</a>{% if categorization %} > <a href="{{ root() }}{{ categorization }}">{{ categorization.capitalize() }}</a>{% endif %}{% if title %} > {{ title }}{% endif %}</h1>
|
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a>{% if categorization %} > <a href="{{ root() }}/{{ categorization }}/{{ index() }}">{{ categorization.capitalize() }}</a>{% endif %}{% if title %} > {{ title }}{% endif %}</h1>
|
||||||
{% include 'nav.html' %}
|
{% include 'nav.html' %}
|
||||||
<div class="card-listing">
|
<div class="card-listing">
|
||||||
{% for work in works %}
|
{% for work in works %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a href="{{ root() }}works/{{ work['id'] }}/">
|
<a href="{{ root() }}/works/{{ work['id'] }}/{{ index() }}">
|
||||||
<img src="{{ root() }}thumbnails/{{ work['id'] }}.jpg">
|
<img src="{{ root() }}/thumbnails/{{ work['id'] }}.jpg">
|
||||||
<div class="card-authors">
|
<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 %}]
|
[{% 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>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
{% from 'utils.html' import root with context %}
|
{% from 'utils.html' import root, index with context %}
|
||||||
<div class="nav">{% for c in ['circles', 'authors', 'tags', 'series'] %}<div class="nav-item"><a href="{{ root() }}{{ c }}">{{ c.capitalize() }}</a></div>{% endfor %}</div>
|
<div class="nav">{% for c in ['circles', 'authors', 'tags', 'series'] %}<div class="nav-item"><a href="{{ root() }}/{{ c }}/{{ index() }}">{{ c.capitalize() }}</a></div>{% endfor %}</div>
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
{% macro root() %}{% for i in range(depth) %}../{% endfor %}{% endmacro %}
|
{% macro root() %}{% if depth == 0 %}.{% else %}..{% endif %}{{ '/..' * (depth-1) }}{% endmacro %}
|
||||||
|
{% macro index() %}{% if not noindex %}index.html{% endif %}{% endmacro %}
|
||||||
{% macro urlcat(s) %}{{ s | replace('/', ' ') | urlencode }}{% endmacro %}
|
{% macro urlcat(s) %}{{ s | replace('/', ' ') | urlencode }}{% endmacro %}
|
||||||
|
|
|
@ -1,20 +1,38 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from 'utils.html' import root with context %}
|
{% from 'utils.html' import index, root with context %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ root() }}static/viewer.css">
|
<link rel="stylesheet" type="text/css" href="{{ root() }}/static/viewer.css">
|
||||||
<script>
|
<script>
|
||||||
const WORK_ID = "{{ work['id'] }}";
|
const WORK_ID = "{{ work['id'] }}";
|
||||||
|
const INDEX = "{{ index() }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ root() }}static/viewer.js"></script>
|
<script src="{{ root() }}/static/viewer.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="viewer-images">
|
<div id="viewer-images">
|
||||||
{% for filename in images %}
|
{% for filename in images %}
|
||||||
<img src="{{ root() }}images/{{ work['id'] }}/{{ filename }}" class="viewer-image">
|
<img src="{{ root() }}/images/{{ work['id'] }}/{{ filename }}" class="viewer-image">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div id="progress"></div>
|
<div id="progress"></div>
|
||||||
<div id="page-num"></div>
|
<div id="page-num"></div>
|
||||||
<div id="duration"></div>
|
<div id="duration"></div>
|
||||||
|
<div id="controls">
|
||||||
|
<div id="tap-left" class="tap">
|
||||||
|
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='80' height='140' viewBox='-10 -70 80 140'>
|
||||||
|
<path d='M 60 -60 L 0 0 L 60 60' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="tap-right" class="tap">
|
||||||
|
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='80' height='140' viewBox='-10 -70 80 140'>
|
||||||
|
<path d='M 0 -60 L 60 0 L 0 60' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="tap-back" class="tap">
|
||||||
|
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='140' height='80' viewBox='-70 -10 140 80'>
|
||||||
|
<path d='M -60 60 L 0 0 L 60 60' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="image-container"></div>
|
<div id="image-container"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from 'utils.html' import urlcat, root with context %}
|
{% from 'utils.html' import urlcat, root, index with context %}
|
||||||
<h1 id="title"><a href="{{ root() }}">DL</a> > {{ title }}</h1>
|
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DL</a> > {{ title }}</h1>
|
||||||
<div class="work-container">
|
<div class="work-container">
|
||||||
<div class="work-preview">
|
<div class="work-preview">
|
||||||
<a href="view/">
|
<a href="view/{{ index() }}">
|
||||||
<img src="{{ root() }}thumbnails/{{ work['id'] }}.jpg">
|
<img src="{{ root() }}/thumbnails/{{ work['id'] }}.jpg">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="work-info">
|
<div class="work-info">
|
||||||
|
@ -13,28 +13,29 @@
|
||||||
{% if work['circle'] %}
|
{% if work['circle'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Circle</th>
|
<th>Circle</th>
|
||||||
<td><a class="work-info-link" href="{{ root() }}circles/{{ urlcat(work['circle']) }}">{{ work['circle'] }}</a></td>
|
<td><a class="work-info-link" href="{{ root() }}/circles/{{ urlcat(work['circle']) }}/{{ index() }}">{{ work['circle'] }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if work['authors'] %}
|
{% if work['authors'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Authors</th>
|
<th>Authors</th>
|
||||||
<td>{% for author in work['authors'] %}<a class="work-info-link" href="{{ root() }}authors/{{ urlcat(author) }}">{{ author }}</a> {% endfor %}</td>
|
<td>{% for author in work['authors'] %}<a class="work-info-link" href="{{ root() }}/authors/{{ urlcat(author) }}/{{ index() }}">{{ author }}</a> {% endfor %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if work['tags'] %}
|
{% if work['tags'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tags</th>
|
<th>Tags</th>
|
||||||
<td>{% for tag in work['tags'] %}<a class="work-info-link" href="{{ root() }}tags/{{ urlcat(tag) }}">{{ tag }}</a> {% endfor %}</td>
|
<td>{% for tag in work['tags'] %}<a class="work-info-link" href="{{ root() }}/tags/{{ urlcat(tag) }}/{{ index() }}">{{ tag }}</a> {% endfor %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if work['series'] %}
|
{% if work['series'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Series</th>
|
<th>Series</th>
|
||||||
<td><a class="work-info-link" href="{{ root() }}series/{{ urlcat(work['series']) }}">{{ work['series'] }}</a></td>
|
<td><a class="work-info-link" href="{{ root() }}/series/{{ urlcat(work['series']) }}/{{ index() }}">{{ work['series'] }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
{{ work['description'] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
[project]
|
[project]
|
||||||
name = "dlibrary"
|
name = "dlibrary"
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
description = "Cataloging tool and viewer for downloaded DLSite purchases"
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
authors = [{name = "xenofem"}]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests",
|
"requests",
|
||||||
"PyMuPDF",
|
"PyMuPDF",
|
||||||
|
|
Loading…
Reference in a new issue