Repos / pytaku / 2688ad52cc
commit 2688ad52ccc8e3966f49a5914496e7fbaf21b285
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Wed Aug 5 20:25:45 2020 +0700
show my follows
next: implement reading history/progress
diff --git a/pyproject.toml b/pyproject.toml
index 0ec75e8..0726a55 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytaku"
-version = "0.1.3"
+version = "0.1.4"
description = ""
authors = ["Bùi Thành Nhân <hi@imnhan.com>"]
license = "AGPL-3.0-only"
diff --git a/src/pytaku/database/common.py b/src/pytaku/database/common.py
index 6d51c1f..6701431 100644
--- a/src/pytaku/database/common.py
+++ b/src/pytaku/database/common.py
@@ -12,3 +12,9 @@ def get_conn():
# Apparently you need to enable this pragma _per connection_
_conn.cursor().execute("PRAGMA foreign_keys = ON;")
return _conn
+
+
+def run_sql(*args, **kwargs):
+ cursor = get_conn().cursor()
+ results = cursor.execute(*args, **kwargs)
+ return list(results)
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 61b98c0..170d40a 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -17,13 +17,17 @@
from . import mangadex
from .conf import config
+from .decorators import require_login
from .persistence import (
+ follow,
+ get_followed_titles,
get_prev_next_chapters,
load_chapter,
load_title,
register_user,
save_chapter,
save_title,
+ unfollow,
verify_username_password,
)
@@ -35,14 +39,30 @@
@app.route("/")
def home_view():
+ if session.get("user"):
+ return redirect(url_for("follows_view"))
return render_template("home.html")
-@app.route("/follow", methods=["POST"])
-def follow_view():
- title_id = request.form.get("title_id")
- site = request.form.get("site", "mangadex")
- return redirect(url_for(""))
+@app.route("/me", methods=["GET"])
+@require_login
+def follows_view():
+ titles = get_followed_titles(session["user"]["id"])
+ return render_template("follows.html", titles=titles)
+
+
+@app.route("/follow/<site>/<title_id>", methods=["POST"])
+@require_login
+def follow_view(site, title_id):
+ follow(session["user"]["id"], site, title_id)
+ return redirect(url_for("title_view", site=site, title_id=title_id))
+
+
+@app.route("/unfollow/<site>/<title_id>", methods=["POST"])
+@require_login
+def unfollow_view(site, title_id):
+ unfollow(session["user"]["id"], site, title_id)
+ return redirect(url_for("title_view", site=site, title_id=title_id))
@app.route("/logout", methods=["POST"])
@@ -107,14 +127,16 @@ def auth_view():
if not (username and password):
message = "Empty field(s) spotted. Protip: spaces don't count."
status_code = 400
- elif not verify_username_password(username, password):
- message = "Wrong username/password combination."
- status_code = 400
- else: # success!
- resp = redirect(request.args.get("next", url_for("home_view")))
- session.permanent = remember
- session["user"] = {"username": username}
- return resp
+ else:
+ user_id = verify_username_password(username, password)
+ if user_id is None:
+ message = "Wrong username/password combination."
+ status_code = 400
+ else: # success!
+ resp = redirect(request.args.get("next", url_for("home_view")))
+ session.permanent = remember
+ session["user"] = {"username": username, "id": user_id}
+ return resp
return (
render_template(
@@ -134,7 +156,9 @@ def auth_view():
@app.route("/title/<site>/<title_id>")
def title_view(site, title_id):
- title = load_title(site, title_id)
+ user = session.get("user", None)
+ user_id = user["id"] if user else None
+ title = load_title(site, title_id, user_id=user_id)
if not title:
print("Getting title", title_id)
title = get_title(title_id)
diff --git a/src/pytaku/persistence.py b/src/pytaku/persistence.py
index f8c81ee..41e0a00 100644
--- a/src/pytaku/persistence.py
+++ b/src/pytaku/persistence.py
@@ -3,7 +3,7 @@
import apsw
import argon2
-from .database.common import get_conn
+from .database.common import get_conn, run_sql
def save_title(title):
@@ -47,7 +47,7 @@ def save_title(title):
)
-def load_title(site, title_id):
+def load_title(site, title_id, user_id=None):
conn = get_conn()
result = list(
conn.cursor().execute(
@@ -67,7 +67,8 @@ def load_title(site, title_id):
raise Exception(f"Found multiple results for title_id {title_id} on {site}!")
else:
title = result[0]
- return {
+
+ return_val = {
"id": title[0],
"name": title[1],
"site": title[2],
@@ -77,6 +78,15 @@ def load_title(site, title_id):
"descriptions": json.loads(title[6]),
}
+ if user_id is not None:
+ return_val["is_following"] = bool(
+ run_sql(
+ "SELECT 1 FROM follow WHERE user_id=? AND site=? AND title_id=?;",
+ (user_id, site, return_val["id"]),
+ )
+ )
+ return return_val
+
def save_chapter(chapter):
conn = get_conn()
@@ -186,17 +196,54 @@ def verify_username_password(username, password):
data = list(
get_conn()
.cursor()
- .execute("SELECT password FROM user WHERE username = ?;", (username,))
+ .execute("SELECT id, password FROM user WHERE username = ?;", (username,))
)
if len(data) != 1:
print(f"User {username} doesn't exist.")
- return False
+ return None
+
+ user_id = data[0][0]
+ hashed_password = data[0][1]
hasher = argon2.PasswordHasher()
- hash = data[0][0]
try:
- hasher.verify(hash, password)
- return True
+ hasher.verify(hashed_password, password)
+ return user_id
except argon2.exceptions.VerifyMismatchError:
print(f"User {username} exists but password doesn't match.")
- return False
+ return None
+
+
+def follow(user_id, site, title_id):
+ get_conn().cursor().execute(
+ "INSERT INTO follow (user_id, site, title_id) VALUES (?, ?, ?);",
+ (user_id, site, title_id),
+ )
+
+
+def unfollow(user_id, site, title_id):
+ get_conn().cursor().execute(
+ "DELETE FROM follow WHERE user_id=? AND site=? AND title_id=?;",
+ (user_id, site, title_id),
+ )
+
+
+def get_followed_titles(user_id):
+ titles = run_sql(
+ """
+ SELECT t.id, t.site, t.name, t.cover_ext, t.chapters
+ FROM title t
+ INNER JOIN follow f ON f.title_id = t.id AND f.site = t.site
+ INNER JOIN user u ON u.id = f.user_id
+ WHERE user_id=?;
+ """,
+ (user_id,),
+ )
+ keys = ("id", "site", "name", "cover_ext", "chapters")
+ title_dicts = []
+ for t in titles:
+ title = {key: t[i] for i, key in enumerate(keys)}
+ title["chapters"] = json.loads(title["chapters"])
+ title_dicts.append(title)
+
+ return title_dicts
diff --git a/src/pytaku/templates/base.html b/src/pytaku/templates/base.html
index 6fef79e..a917622 100644
--- a/src/pytaku/templates/base.html
+++ b/src/pytaku/templates/base.html
@@ -1,9 +1,9 @@
{# vim: ft=htmldjango
#}
-{% macro ibutton(href='', left_icon='', right_icon='', text='', color='red', disabled=False) -%}
+{% macro ibutton(href='', left_icon='', right_icon='', text='', color='red', title='', disabled=False) -%}
{% set element = 'a' if href else 'button' %}
-<{{ element }} class="{{ color }} button {% if disabled %}disabled{% endif %}"
+<{{ element }} class="{{ color }} button {% if disabled %}disabled{% endif %}" title="{{title}}"
{% if href %}href="{{ href }}"{% endif %}
{% if disabled %}disabled{% endif %} >
{% if left_icon %}
diff --git a/src/pytaku/templates/chapter.html b/src/pytaku/templates/chapter.html
index 04aa572..d506c62 100644
--- a/src/pytaku/templates/chapter.html
+++ b/src/pytaku/templates/chapter.html
@@ -1,8 +1,7 @@
{% extends 'base.html' %}
{% block title %}
-Ch. {{ num_major }}
-{% if num_minor %}.{{ num_minor }}{% endif %}
+Ch.{{ num_major }}{% if num_minor %}.{{ num_minor }}{% endif %}
{% if name %} - {{ name }}{% endif %}
{% endblock %}
diff --git a/src/pytaku/templates/follows.html b/src/pytaku/templates/follows.html
new file mode 100644
index 0000000..906df62
--- /dev/null
+++ b/src/pytaku/templates/follows.html
@@ -0,0 +1,89 @@
+{% extends 'base.html' %}
+
+{% block title %}
+Stuff I follow
+{% endblock %}
+
+{% block head %}
+<style>
+.title {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin-bottom: var(--body-padding);
+ background-color: #efefef;
+}
+
+.cover {
+ border: 1px solid #777;
+ margin-right: .5rem;
+}
+.cover:hover {
+ box-shadow: 0 0 3px black;
+}
+
+.chapters {
+ padding: .5rem .5rem .5rem 0;
+}
+
+.chapter {
+ display: block;
+ margin-bottom: .5rem;
+ background-color: white;
+ border: 1px solid #999;
+ padding: 6px;
+ border-radius: 5px;
+ text-decoration: none;
+ color: black;
+}
+.chapter:hover {
+ background-color: #eee;
+}
+
+.group {
+ font-size: .9em;
+ background-color: #ddd;
+ border-radius: 3px;
+ white-space: nowrap;
+ padding: 2px 5px;
+}
+
+.more {
+ display: inline-block;
+ font-style: italic;
+}
+</style>
+{% endblock %}
+
+{% block content %}
+
+
+{% for title in titles %}
+{% set title_url = url_for('title_view', site=title['site'], title_id=title['id']) %}
+<div class="title">
+ <div>
+ <a href="{{ title_url }}">
+ <img class="cover"
+ src="https://mangadex.org/images/manga/{{ title['id'] }}.large.{{ title['cover_ext'] }}"
+ alt="{{ title['name'] }}" />
+ </a>
+ </div>
+ <div class="chapters">
+ {% for ch in title['chapters'][:6] %}
+ <a class="chapter" href="{{ url_for('chapter_view', site=title['site'], chapter_id=ch['id']) }}">
+ Chapter {{ ch['num_major'] }}{% if ch['num_minor'] %}.{{ ch['num_minor'] }}{% endif %}
+ {% if ch['volume'] %}Volume {{ ch['volume'] }} {% endif %}
+ {% if ch['name'] %} - {{ ch['name'] }}{% endif %}
+ {% for group in ch['groups'] %}
+ <span class="group">{{ group | truncate(20) }}</span>
+ {% endfor %}
+ </a>
+ {% endfor %}
+ {% if title['chapters']|length > 6 %}
+ <a class="more chapter" href="{{ title_url }}">and more...</a>
+ {% endif %}
+ </div>
+</div>
+{% endfor %}
+
+{% endblock %}
diff --git a/src/pytaku/templates/title.html b/src/pytaku/templates/title.html
index 5c52ef3..3ed3462 100644
--- a/src/pytaku/templates/title.html
+++ b/src/pytaku/templates/title.html
@@ -6,6 +6,10 @@
width: 400px;
border: 1px solid black;
}
+
+ .details > form {
+ display: inline-block;
+ }
</style>
{% endblock %}
@@ -15,9 +19,25 @@
{% block content %}
-<div>
-<h1>{{ name }}</h1>
-{{ ibutton(href='https://mangadex.org/manga/' + id, right_icon='arrow-up-right', text='Source site', color='blue') }}
+<div class="details">
+ <h1>{{ name }}</h1>
+ {% if session['user'] %}
+ {% if is_following %}
+ {% set fview = 'unfollow_view' %}
+ {% set ftext = 'Following' %}
+ {% set fcolor = 'red' %}
+ {% set ftitle = 'Click to unfollow' %}
+ {% else %}
+ {% set fview = 'follow_view' %}
+ {% set ftext = 'Follow' %}
+ {% set fcolor = 'green' %}
+ {% set ftitle = 'Click to follow' %}
+ {% endif %}
+ <form action="{{ url_for(fview, site=site, title_id=id) }}" method="POST">
+ {{ ibutton(left_icon='bookmark', text=ftext, color=fcolor, title=ftitle) }}
+ </form>
+ {% endif %}
+ {{ ibutton(href='https://mangadex.org/manga/' + id, right_icon='arrow-up-right', text='Source site', color='blue', title='Go to source site') }}
</div>
<img class="cover" src="https://mangadex.org/images/manga/{{ id }}.{{ cover_ext }}" alt="cover" />