Goon — self-hosted aggregator for adult-content scene metadata. Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites. Cross-source deduplication via perceptual hash + Levenshtein distance. FastAPI backend + APScheduler worker + React Native (Expo) mobile client. FOSS, ad-free, donation-funded. See README for details.
83 lines
2.7 KiB
HTML
83 lines
2.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Candidate · goon{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="toolbar">
|
|
<a href="/ui/">← back</a>
|
|
<span class="muted">{{ cand.kind }} · {{ cand.status }}</span>
|
|
<span class="score {{ score_class(cand.score) }}">{{ '%.2f' % cand.score }}</span>
|
|
</div>
|
|
|
|
{% if cand.status == "pending" %}
|
|
<div class="card" id="actions">
|
|
<form
|
|
method="post"
|
|
hx-post="/ui/candidate/{{ cand.id }}/resolve"
|
|
hx-target="#actions"
|
|
hx-swap="outerHTML"
|
|
style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;"
|
|
>
|
|
<button name="action" value="merge_keep_left" class="primary">Merge → keep LEFT</button>
|
|
<button name="action" value="merge_keep_right" class="primary">Merge → keep RIGHT</button>
|
|
<button name="action" value="reject" class="danger">Reject (keep both)</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="card muted">Resolved.</div>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
<div class="col card">
|
|
<h2>LEFT</h2>
|
|
{% if cand.left %}
|
|
{{ scene_block(cand.left) }}
|
|
{% else %}
|
|
<div class="muted">scene missing (id={{ cand.left_id }})</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col card">
|
|
<h2>RIGHT</h2>
|
|
{% if cand.right %}
|
|
{{ scene_block(cand.right) }}
|
|
{% else %}
|
|
<div class="muted">scene missing (id={{ cand.right_id }})</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Reasons</h2>
|
|
<pre>{{ cand.reasons | tojson(indent=2) }}</pre>
|
|
</div>
|
|
|
|
{% macro scene_block(s) %}
|
|
<div class="stack">
|
|
<div><strong>{{ s.title }}</strong></div>
|
|
<div class="muted">
|
|
{{ s.release_date or '?' }}
|
|
{% if s.studio %} · {{ s.studio.name }}{% endif %}
|
|
{% if s.duration_sec %} · {{ '%dm%02ds' % (s.duration_sec // 60, s.duration_sec % 60) }}{% endif %}
|
|
</div>
|
|
{% if s.code %}<div class="muted">code: {{ s.code }}</div>{% endif %}
|
|
<div>
|
|
{% for p in s.performers %}
|
|
<span class="pill">
|
|
{{ p.canonical_name }}{% if p.as_alias %} <span class="muted">as {{ p.as_alias }}</span>{% endif %}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
<div>
|
|
{% for t in s.tags %}<span class="pill">{{ t.name }}</span>{% endfor %}
|
|
</div>
|
|
<div class="muted">
|
|
sources:
|
|
{% for r in s.external_refs %}
|
|
<span class="pill">{{ r.source }}{% if r.external_id %} · {{ r.external_id[:8] }}{% endif %}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% if s.description %}
|
|
<div class="muted" style="margin-top:8px;">{{ s.description[:280] }}{% if s.description|length > 280 %}…{% endif %}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
{% endblock %}
|