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 importlib_resources as resources
|
||||
from pathlib import Path
|
||||
from os import getenv
|
||||
from os.path import relpath, splitext
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import textwrap
|
||||
from urllib.parse import urlparse
|
||||
import zipfile
|
||||
|
||||
|
@ -296,12 +298,15 @@ def metadata(args):
|
|||
|
||||
con.close()
|
||||
|
||||
def copy_contents(src, dest):
|
||||
def copy_recursive(src, dest):
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
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)
|
||||
|
||||
def publish(args):
|
||||
def generate(args):
|
||||
jenv = Environment(
|
||||
loader=PackageLoader("dlibrary"),
|
||||
autoescape=select_autoescape()
|
||||
|
@ -347,7 +352,7 @@ def publish(args):
|
|||
with open(viewer_dir / 'index.html', 'w') as f:
|
||||
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
|
||||
|
||||
cats = [cat for (cat,) in cur.execute(query)]
|
||||
|
@ -374,6 +379,7 @@ def publish(args):
|
|||
categorization=categorization,
|
||||
categories=cats,
|
||||
samples=cat_samples,
|
||||
work_style_cards=work_style_cards,
|
||||
))
|
||||
|
||||
make_categorization(
|
||||
|
@ -395,10 +401,11 @@ def publish(args):
|
|||
'series',
|
||||
'SELECT DISTINCT series FROM works WHERE series NOT NULL ORDER BY series',
|
||||
lambda series: lambda work: work['series'] == series,
|
||||
work_style_cards=True,
|
||||
)
|
||||
|
||||
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:
|
||||
f.write(list_template.render(depth=0, works=works))
|
||||
|
@ -406,12 +413,33 @@ def publish(args):
|
|||
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(
|
||||
'-d', '--destdir',
|
||||
type=Path,
|
||||
default=Path('./dlibrary'),
|
||||
help='directory to store dlibrary content and metadata to (default: ./dlibrary)',
|
||||
default=Path(getenv('DLIBRARY_DIR', './dlibrary')),
|
||||
help='directory to store dlibrary content and metadata to (default: $DLIBRARY_DIR or ./dlibrary)',
|
||||
)
|
||||
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.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(
|
||||
'hints',
|
||||
metavar='PATH',
|
||||
type=Path,
|
||||
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_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(
|
||||
'paths',
|
||||
metavar='PATH',
|
||||
type=Path,
|
||||
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)
|
||||
|
||||
|
@ -462,8 +546,20 @@ parser_metadata.add_argument(
|
|||
)
|
||||
parser_metadata.set_defaults(func=metadata)
|
||||
|
||||
parser_publish = subparsers.add_parser('publish', help='generate HTML/CSS/JS for library site')
|
||||
parser_publish.set_defaults(func=publish)
|
||||
parser_generate = subparsers.add_parser(
|
||||
'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():
|
||||
args = argparser.parse_args()
|
||||
|
|
|
@ -51,12 +51,19 @@ body {
|
|||
|
||||
.work-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.work-preview img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.work-info {
|
||||
max-width: min(50%, 500px);
|
||||
flex-basis: 40%;
|
||||
flex-grow: 1;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.work-info td, .work-info th {
|
||||
|
|
|
@ -4,3 +4,39 @@ html, body {
|
|||
padding: 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 interval;
|
||||
let elapsed = 0;
|
||||
let rtl = (localStorage.getItem(`${WORK_ID}-rtl`) !== "false");
|
||||
|
||||
function startTimer() {
|
||||
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);
|
||||
changeDuration(duration, paused);
|
||||
|
||||
document.onkeydown = event =>{
|
||||
document.onkeydown = event => {
|
||||
hideTapZones();
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 32: //space
|
||||
changeDuration(duration, !paused);
|
||||
break;
|
||||
case 37: //left
|
||||
changePage(currentPage - 1);
|
||||
left();
|
||||
break;
|
||||
case 38: //up
|
||||
if (2 <= duration && duration <= 10) {
|
||||
|
@ -99,7 +130,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
break;
|
||||
case 39: //right
|
||||
changePage(currentPage + 1);
|
||||
right();
|
||||
break;
|
||||
case 40: //down
|
||||
if (duration < 10) {
|
||||
|
@ -111,10 +142,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
break;
|
||||
case 13: //enter
|
||||
changeDuration(duration, true);
|
||||
localStorage.setItem(`${WORK_ID}-currentPage`, 0);
|
||||
window.location.href = "../";
|
||||
exitToWork();
|
||||
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="color-scheme" content="dark">
|
||||
<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 %}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ categorization.capitalize() }} - DLibrary{% endblock %}
|
||||
{% block body %}
|
||||
{% from 'utils.html' import urlcat, root with context %}
|
||||
<h1 id="title"><a href="{{ root() }}">DLibrary</a> > <a href="{{ root() }}{{ categorization }}">{{ categorization.capitalize() }}</a></h1>
|
||||
{% from 'utils.html' import urlcat, index, root with context %}
|
||||
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a> > {{ categorization.capitalize() }}</h1>
|
||||
{% include 'nav.html' %}
|
||||
<div class="card-listing">
|
||||
{% for cat in categories %}
|
||||
<div class="card category">
|
||||
<a href="{{ root() }}{{ categorization }}/{{ urlcat(cat) }}/">
|
||||
<div class="card {% if not work_style_cards %}category{% endif %}">
|
||||
<a href="{{ root() }}/{{ categorization }}/{{ urlcat(cat) }}/{{ index() }}">
|
||||
<div class="card-title">
|
||||
{{ cat }}
|
||||
</div>
|
||||
{% if samples[cat] %}
|
||||
<img src="{{ root() }}thumbnails/{{ samples[cat]['id'] }}.jpg">
|
||||
<img src="{{ root() }}/thumbnails/{{ samples[cat]['id'] }}.jpg">
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
{% from 'utils.html' import 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>
|
||||
{% from 'utils.html' import index, 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>
|
||||
{% include 'nav.html' %}
|
||||
<div class="card-listing">
|
||||
{% for work in works %}
|
||||
<div class="card">
|
||||
<a href="{{ root() }}works/{{ work['id'] }}/">
|
||||
<img src="{{ root() }}thumbnails/{{ work['id'] }}.jpg">
|
||||
<a href="{{ root() }}/works/{{ work['id'] }}/{{ index() }}">
|
||||
<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>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
{% 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 }}">{{ c.capitalize() }}</a></div>{% endfor %}</div>
|
||||
{% 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 }}/{{ 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 %}
|
||||
|
|
|
@ -1,20 +1,38 @@
|
|||
{% extends 'base.html' %}
|
||||
{% from 'utils.html' import root with context %}
|
||||
{% from 'utils.html' import index, root with context %}
|
||||
{% 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>
|
||||
const WORK_ID = "{{ work['id'] }}";
|
||||
const INDEX = "{{ index() }}";
|
||||
</script>
|
||||
<script src="{{ root() }}static/viewer.js"></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">
|
||||
<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="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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block body %}
|
||||
{% from 'utils.html' import urlcat, root with context %}
|
||||
<h1 id="title"><a href="{{ root() }}">DL</a> > {{ title }}</h1>
|
||||
{% from 'utils.html' import urlcat, root, index with context %}
|
||||
<h1 id="title"><a href="{{ root() }}/{{ index() }}">DL</a> > {{ title }}</h1>
|
||||
<div class="work-container">
|
||||
<div class="work-preview">
|
||||
<a href="view/">
|
||||
<img src="{{ root() }}thumbnails/{{ work['id'] }}.jpg">
|
||||
<a href="view/{{ index() }}">
|
||||
<img src="{{ root() }}/thumbnails/{{ work['id'] }}.jpg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="work-info">
|
||||
|
@ -13,28 +13,29 @@
|
|||
{% if work['circle'] %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if work['authors'] %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if work['tags'] %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if work['series'] %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endif %}
|
||||
</table>
|
||||
{{ work['description'] }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
[project]
|
||||
name = "dlibrary"
|
||||
version = "0.1"
|
||||
description = "Cataloging tool and viewer for downloaded DLSite purchases"
|
||||
license = {file = "LICENSE"}
|
||||
authors = [{name = "xenofem"}]
|
||||
dependencies = [
|
||||
"requests",
|
||||
"PyMuPDF",
|
||||
|
|
Loading…
Reference in a new issue