Repos / pytaku / 69a8aa0ef7
commit 69a8aa0ef75012f7f83d964e1d8d8c925820327d
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Sat Aug 1 21:59:32 2020 +0700

    render search results

diff --git a/README.md b/README.md
index bccf335..c45f7c9 100644
--- a/README.md
+++ b/README.md
@@ -6,4 +6,7 @@
       --global-option=build --global-option=--enable-all-extensions
 
 FLASK_ENV=development FLASK_APP=pytaku.main:app flask run
+
+pytaku-generate-config > pytaku.conf.json
+# fill stuff as needed
 ```
diff --git a/poetry.lock b/poetry.lock
index 4654731..da61531 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,56 +1,66 @@
 [[package]]
-category = "main"
-description = "Python package for providing Mozilla's CA Bundle."
 name = "certifi"
+version = "2020.6.20"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
 optional = false
 python-versions = "*"
-version = "2020.6.20"
 
 [[package]]
-category = "main"
-description = "Universal encoding detector for Python 2 and 3"
 name = "chardet"
+version = "3.0.4"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
 optional = false
 python-versions = "*"
-version = "3.0.4"
 
 [[package]]
-category = "main"
-description = "Composable command line interface toolkit"
 name = "click"
+version = "7.1.2"
+description = "Composable command line interface toolkit"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "7.1.2"
 
 [[package]]
-category = "main"
-description = "A simple framework for building complex web applications."
 name = "flask"
+version = "1.1.2"
+description = "A simple framework for building complex web applications."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "1.1.2"
-
-[package.dependencies]
-Jinja2 = ">=2.10.1"
-Werkzeug = ">=0.15"
-click = ">=5.1"
-itsdangerous = ">=0.24"
 
 [package.extras]
 dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
 docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
 dotenv = ["python-dotenv"]
 
+[package.dependencies]
+click = ">=5.1"
+itsdangerous = ">=0.24"
+Jinja2 = ">=2.10.1"
+Werkzeug = ">=0.15"
+
 [[package]]
+name = "goodconf"
+version = "1.0.0"
+description = "Load configuration variables from a file or environment"
 category = "main"
-description = "WSGI HTTP Server for UNIX"
+optional = false
+python-versions = "*"
+
+[package.extras]
+maintainer = ["zest.releaser"]
+tests = ["django (<2.1)", "ruamel.yaml", "pytest (3.5.0)", "pytest-cov (2.5.1)", "pytest-mock (1.7.1)"]
+yaml = ["ruamel.yaml"]
+
+[[package]]
 name = "gunicorn"
+version = "20.0.4"
+description = "WSGI HTTP Server for UNIX"
+category = "main"
 optional = false
 python-versions = ">=3.4"
-version = "20.0.4"
-
-[package.dependencies]
-setuptools = ">=3.0"
 
 [package.extras]
 eventlet = ["eventlet (>=0.9.7)"]
@@ -58,51 +68,58 @@ gevent = ["gevent (>=0.13)"]
 setproctitle = ["setproctitle"]
 tornado = ["tornado (>=0.2)"]
 
+[package.dependencies]
+setuptools = ">=3.0"
+
 [[package]]
-category = "main"
-description = "Internationalized Domain Names in Applications (IDNA)"
 name = "idna"
+version = "2.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.10"
 
 [[package]]
-category = "main"
-description = "Various helpers to pass data to untrusted environments and back."
 name = "itsdangerous"
+version = "1.1.0"
+description = "Various helpers to pass data to untrusted environments and back."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.1.0"
 
 [[package]]
-category = "main"
-description = "A very fast and expressive template engine."
 name = "jinja2"
+version = "2.11.2"
+description = "A very fast and expressive template engine."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.11.2"
-
-[package.dependencies]
-MarkupSafe = ">=0.23"
 
 [package.extras]
 i18n = ["Babel (>=0.8)"]
 
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
 [[package]]
-category = "main"
-description = "Safely add untrusted strings to HTML/XML markup."
 name = "markupsafe"
+version = "1.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
-version = "1.1.1"
 
 [[package]]
-category = "main"
-description = "Python HTTP for Humans."
 name = "requests"
+version = "2.24.0"
+description = "Python HTTP for Humans."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.24.0"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
 
 [package.dependencies]
 certifi = ">=2017.4.17"
@@ -110,17 +127,13 @@ chardet = ">=3.0.2,<4"
 idna = ">=2.5,<3"
 urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
 
-[package.extras]
-security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
-socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
-
 [[package]]
-category = "main"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
 name = "urllib3"
+version = "1.25.10"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
-version = "1.25.10"
 
 [package.extras]
 brotli = ["brotlipy (>=0.6.0)"]
@@ -128,21 +141,21 @@ secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0
 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
 
 [[package]]
-category = "main"
-description = "The comprehensive WSGI web application library."
 name = "werkzeug"
+version = "1.0.1"
+description = "The comprehensive WSGI web application library."
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "1.0.1"
 
 [package.extras]
 dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
 watchdog = ["watchdog"]
 
 [metadata]
-content-hash = "c1d5cedfcf8897fb539d7bbd395966ad5c864b9a57737ae54e9ae099e9600026"
 lock-version = "1.0"
 python-versions = "^3.7"
+content-hash = "4c5557f7eb25c9e75ce9983a3ba5cabcddb59a6d26ff11185732aa13f954a326"
 
 [metadata.files]
 certifi = [
@@ -161,6 +174,10 @@ flask = [
     {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
     {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
 ]
+goodconf = [
+    {file = "goodconf-1.0.0-py2.py3-none-any.whl", hash = "sha256:beb2f9ed734015e1becd4338d8b1e363cf51fb52e2f794f4e85e8c59d097442e"},
+    {file = "goodconf-1.0.0.tar.gz", hash = "sha256:2c33460b4d9859ffacff32355b7effb1a922a16c1d54e8edd6452503bd8e809b"},
+]
 gunicorn = [
     {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
     {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
diff --git a/pyproject.toml b/pyproject.toml
index cf9cb6b..37c1cb5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,12 +11,14 @@ packages = [
 
 [tool.poetry.scripts]
 pytaku-migrate = "pytaku:migrate"
+pytaku-generate-config = "pytaku:generate_config"
 
 [tool.poetry.dependencies]
 python = "^3.7"
 flask = "^1.1.2"
 gunicorn = "^20.0.4"
 requests = "^2.24.0"
+goodconf = "^1.0.0"
 
 [tool.poetry.dev-dependencies]
 
diff --git a/src/pytaku/__init__.py b/src/pytaku/__init__.py
index 53b1563..bd3e1b9 100644
--- a/src/pytaku/__init__.py
+++ b/src/pytaku/__init__.py
@@ -1,3 +1,6 @@
+from pytaku.conf import config
+
+
 def migrate():
     import argparse
     from .database.migrator import migrate
@@ -12,3 +15,7 @@ def migrate():
     args = argparser.parse_args()
 
     migrate(overwrite_latest_schema=args.dev)
+
+
+def generate_config():
+    print(config.generate_json(DEBUG=True))
diff --git a/src/pytaku/conf.py b/src/pytaku/conf.py
new file mode 100644
index 0000000..dc1786e
--- /dev/null
+++ b/src/pytaku/conf.py
@@ -0,0 +1,9 @@
+from goodconf import GoodConf, Value
+
+
+class Config(GoodConf):
+    MANGADEX_USERNAME = Value()
+    MANGADEX_PASSWORD = Value()
+
+
+config = Config(default_files=["pytaku.conf.json"], load=True)
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 9bba60e..8eb9d23 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -2,9 +2,11 @@
 import re
 
 import requests
-from flask import Flask, make_response, render_template, url_for
+from flask import Flask, make_response, render_template, request, url_for
 
-from mangoapi import get_chapter, get_title
+from mangoapi import get_chapter, get_title, search_title
+
+from . import mangadex
 
 app = Flask(__name__)
 
@@ -31,7 +33,12 @@ def chapter_view(chapter_id):
 
 @app.route("/search")
 def search_view():
-    return "TODO"
+    query = request.args.get("q", "").strip()
+    titles = []
+    if query:
+        cookies = mangadex.get_cookies()
+        titles = search_title(cookies, query)
+    return render_template("search.html", titles=titles, query=query)
 
 
 @app.route("/proxy/<b64_url>")
diff --git a/src/pytaku/mangadex.py b/src/pytaku/mangadex.py
new file mode 100644
index 0000000..df5156a
--- /dev/null
+++ b/src/pytaku/mangadex.py
@@ -0,0 +1,17 @@
+from mangoapi import login
+from pytaku.conf import config
+
+assert config.MANGADEX_USERNAME
+assert config.MANGADEX_PASSWORD
+
+_cookies = None
+
+
+def get_cookies():
+    global _cookies
+    if _cookies is None:
+        print("Logging in to mangadex")
+        _cookies = login(config.MANGADEX_USERNAME, config.MANGADEX_PASSWORD)
+    else:
+        print("Reusing mangadex cookies")
+    return _cookies
diff --git a/src/pytaku/static/base.css b/src/pytaku/static/base.css
index f2211b1..c694ac3 100644
--- a/src/pytaku/static/base.css
+++ b/src/pytaku/static/base.css
@@ -157,7 +157,6 @@ .links a:hover {
 
 .content {
   margin: auto;
-  max-width: 800px;
   padding: var(--body-padding);
 }
 .content > * {
diff --git a/src/pytaku/templates/base.html b/src/pytaku/templates/base.html
index 05b0b50..94e7a47 100644
--- a/src/pytaku/templates/base.html
+++ b/src/pytaku/templates/base.html
@@ -39,8 +39,8 @@
   <body>
     <nav>
       <a class="logo" href="/"><img src="{{ url_for('static', filename='pytaku.svg')}}" alt="home" /></a>
-      <form class="search" action="{{ url_for('search_view') }}">
-        <input type="text" placeholder="search manga" /><button class="red button">
+      <form class="search" action="{{ url_for('search_view') }}" method="GET">
+        <input type="text" placeholder="search manga" name="q" value="{{ query }}" /><button class="red button">
           <img src="{{ url_for('static', filename='icons/search.svg')}}" alt="search">
         </button>
       </form>
diff --git a/src/pytaku/templates/chapter.html b/src/pytaku/templates/chapter.html
index c39eb8a..aa11159 100644
--- a/src/pytaku/templates/chapter.html
+++ b/src/pytaku/templates/chapter.html
@@ -12,7 +12,6 @@
   }
 
   .content {
-    max-width: 100%;
     padding: var(--body-padding) 0;
     text-align: center;
   }
diff --git a/src/pytaku/templates/search.html b/src/pytaku/templates/search.html
new file mode 100644
index 0000000..f5240e6
--- /dev/null
+++ b/src/pytaku/templates/search.html
@@ -0,0 +1,60 @@
+{% extends 'base.html' %}
+
+{% block title %}
+  {% if query %}
+    "{{ query }}" search results
+  {% else %}
+    Search
+  {% endif %}
+{% endblock %}
+
+{% block head %}
+<style>
+  .results {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+  }
+  .result {
+    display: inline-flex;
+    flex-direction: column;
+    border: 1px solid var(--bg-black);
+    margin: 0 1rem 1rem 0;
+    background-color: var(--bg-black);
+    color: white;
+    text-decoration: none;
+  }
+  .result span {
+    padding: .5rem;
+    width: 0;
+    min-width: 100%;
+  }
+</style>
+{% endblock %}
+
+{% block content %}
+
+{% if not query %}
+  <h1>Please enter a search query above.</h1>
+
+{% else %}
+  {% if titles %}
+  <h1>Showing {{ titles | length }} result(s) for "{{ query }}":</h1>
+  {% else %}
+  <h1>No results for "{{ query }}".</h1>
+  {% endif %}
+
+  <div class="results">
+  {% for title in titles %}
+    <a class="result" href="{{ url_for('title_view', title_id=title['id']) }}"
+       title="{{ title['name'] }}">
+      <img src="https://mangadex.org/images/manga/{{ title['id'] }}.large.jpg" alt="">
+      <span>{{ title['name'] | truncate(50) }}</span>
+    </a>
+  {% endfor %}
+  </div>
+
+{% endif %}
+
+
+{% endblock %}