Compare commits
No commits in common. "b437fcc79e43a1dee28f1355019ee4018fd938ee" and "3ef82c8024aeda826679a65803ce5bed0e454e61" have entirely different histories.
b437fcc79e
...
3ef82c8024
19
LICENSE
19
LICENSE
|
@ -1,19 +0,0 @@
|
||||||
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,12 +4,10 @@ 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
|
||||||
|
|
||||||
|
@ -298,15 +296,12 @@ def metadata(args):
|
||||||
|
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
def copy_recursive(src, dest):
|
def copy_contents(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 generate(args):
|
def publish(args):
|
||||||
jenv = Environment(
|
jenv = Environment(
|
||||||
loader=PackageLoader("dlibrary"),
|
loader=PackageLoader("dlibrary"),
|
||||||
autoescape=select_autoescape()
|
autoescape=select_autoescape()
|
||||||
|
@ -352,7 +347,7 @@ def generate(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, work_style_cards=False):
|
def make_categorization(categorization, query, work_filter):
|
||||||
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)]
|
||||||
|
@ -379,7 +374,6 @@ def generate(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(
|
||||||
|
@ -401,11 +395,10 @@ def generate(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_recursive(r / 'static', args.destdir / 'site' / 'static')
|
copy_contents(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))
|
||||||
|
@ -413,33 +406,12 @@ def generate(args):
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser(
|
argparser = argparse.ArgumentParser(prog='dlibrary')
|
||||||
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(getenv('DLIBRARY_DIR', './dlibrary')),
|
default=Path('./dlibrary'),
|
||||||
help='directory to store dlibrary content and metadata to (default: $DLIBRARY_DIR or ./dlibrary)',
|
help='directory to store dlibrary content and metadata to (default: ./dlibrary)',
|
||||||
)
|
)
|
||||||
subparsers = argparser.add_subparsers(title="subcommands", required=True)
|
subparsers = argparser.add_subparsers(title="subcommands", required=True)
|
||||||
|
|
||||||
|
@ -461,79 +433,23 @@ 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(
|
parser_collate = subparsers.add_parser('collate', help='collate a single sequence of image files for each work')
|
||||||
'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='paths within extraction folders as collation hints'
|
help='manually-specified paths of subdirectories or PDFs within extraction folders, at most one per work',
|
||||||
)
|
)
|
||||||
parser_collate.set_defaults(func=collate)
|
parser_collate.set_defaults(func=collate)
|
||||||
|
|
||||||
parser_manual_collate = subparsers.add_parser(
|
parser_manual_collate = subparsers.add_parser('manual-collate', help='collate a specific work manually, specifying all paths to include')
|
||||||
'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 within a single work to be collated in sequence',
|
help='paths of files (images to symlink, pdfs to extract) or directories (symlink all images in the directory, no recursion, best-effort sorting)'
|
||||||
)
|
)
|
||||||
parser_manual_collate.set_defaults(func=manual_collate)
|
parser_manual_collate.set_defaults(func=manual_collate)
|
||||||
|
|
||||||
|
@ -546,20 +462,8 @@ parser_metadata.add_argument(
|
||||||
)
|
)
|
||||||
parser_metadata.set_defaults(func=metadata)
|
parser_metadata.set_defaults(func=metadata)
|
||||||
|
|
||||||
parser_generate = subparsers.add_parser(
|
parser_publish = subparsers.add_parser('publish', help='generate HTML/CSS/JS for library site')
|
||||||
'generate',
|
parser_publish.set_defaults(func=publish)
|
||||||
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,19 +51,12 @@ 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 {
|
||||||
flex-basis: 40%;
|
max-width: min(50%, 500px);
|
||||||
flex-grow: 1;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-info td, .work-info th {
|
.work-info td, .work-info th {
|
||||||
|
|
|
@ -4,39 +4,3 @@ 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,7 +5,6 @@ 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) {
|
||||||
|
@ -79,46 +78,16 @@ 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
|
||||||
left();
|
changePage(currentPage - 1);
|
||||||
break;
|
break;
|
||||||
case 38: //up
|
case 38: //up
|
||||||
if (2 <= duration && duration <= 10) {
|
if (2 <= duration && duration <= 10) {
|
||||||
|
@ -130,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 39: //right
|
case 39: //right
|
||||||
right();
|
changePage(currentPage + 1);
|
||||||
break;
|
break;
|
||||||
case 40: //down
|
case 40: //down
|
||||||
if (duration < 10) {
|
if (duration < 10) {
|
||||||
|
@ -142,13 +111,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 13: //enter
|
case 13: //enter
|
||||||
exitToWork();
|
changeDuration(duration, true);
|
||||||
|
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, index, root with context %}
|
{% from 'utils.html' import urlcat, root with context %}
|
||||||
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a> > {{ categorization.capitalize() }}</h1>
|
<h1 id="title"><a href="{{ root() }}">DLibrary</a> > <a href="{{ root() }}{{ categorization }}">{{ categorization.capitalize() }}</a></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 {% if not work_style_cards %}category{% endif %}">
|
<div class="card category">
|
||||||
<a href="{{ root() }}/{{ categorization }}/{{ urlcat(cat) }}/{{ index() }}">
|
<a href="{{ root() }}{{ categorization }}/{{ urlcat(cat) }}/">
|
||||||
<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 index, root with context %}
|
{% from 'utils.html' import root with context %}
|
||||||
<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>
|
<h1 id="title"><a href="{{ root() }}">DLibrary</a>{% if categorization %} > <a href="{{ root() }}{{ categorization }}">{{ 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'] }}/{{ index() }}">
|
<a href="{{ root() }}works/{{ work['id'] }}/">
|
||||||
<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, index with context %}
|
{% from 'utils.html' import root with context %}
|
||||||
<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>
|
<div class="nav">{% for c in ['circles', 'authors', 'tags', 'series'] %}<div class="nav-item"><a href="{{ root() }}{{ c }}">{{ c.capitalize() }}</a></div>{% endfor %}</div>
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
{% macro root() %}{% if depth == 0 %}.{% else %}..{% endif %}{{ '/..' * (depth-1) }}{% endmacro %}
|
{% macro root() %}{% for i in range(depth) %}../{% endfor %}{% 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,38 +1,20 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from 'utils.html' import index, root with context %}
|
{% from 'utils.html' import 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, index with context %}
|
{% from 'utils.html' import urlcat, root with context %}
|
||||||
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DL</a> > {{ title }}</h1>
|
<h1 id="title"><a href="{{ root() }}">DL</a> > {{ title }}</h1>
|
||||||
<div class="work-container">
|
<div class="work-container">
|
||||||
<div class="work-preview">
|
<div class="work-preview">
|
||||||
<a href="view/{{ index() }}">
|
<a href="view/">
|
||||||
<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,29 +13,28 @@
|
||||||
{% 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']) }}/{{ index() }}">{{ work['circle'] }}</a></td>
|
<td><a class="work-info-link" href="{{ root() }}circles/{{ urlcat(work['circle']) }}">{{ 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) }}/{{ index() }}">{{ author }}</a> {% endfor %}</td>
|
<td>{% for author in work['authors'] %}<a class="work-info-link" href="{{ root() }}authors/{{ urlcat(author) }}">{{ 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) }}/{{ index() }}">{{ tag }}</a> {% endfor %}</td>
|
<td>{% for tag in work['tags'] %}<a class="work-info-link" href="{{ root() }}tags/{{ urlcat(tag) }}">{{ 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']) }}/{{ index() }}">{{ work['series'] }}</a></td>
|
<td><a class="work-info-link" href="{{ root() }}series/{{ urlcat(work['series']) }}">{{ work['series'] }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{{ work['description'] }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
[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