Repos / pytaku / 5b6adcf900
commit 5b6adcf900b368eb3ee5072ff7513c322579d7f1
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Sat Jan 23 21:05:31 2021 +0700

    parse bbcode from mangadex descriptions
    
    To do that, our system now also accepts descriptions in html format in
    addition to plaintext. Each SourceSite class should specify which format
    it outputs. BBCode can now be converted into html at the source site
    level.

diff --git a/poetry.lock b/poetry.lock
index 6264af2..92ef442 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -56,6 +56,14 @@ optional = false
 python-versions = "*"
 marker = "python_version >= \"3.4\""
 
+[[package]]
+name = "bbcode"
+version = "1.1.0"
+description = "A pure python bbcode parser and formatter."
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "certifi"
 version = "2020.6.20"
@@ -544,7 +552,7 @@ testing = ["jaraco.itertools", "func-timeout"]
 [metadata]
 lock-version = "1.0"
 python-versions = "^3.7"
-content-hash = "3cfb3337e3938185bd5cf650d8258403f4e2227115904da0972cba109818ebf5"
+content-hash = "1753bef74f04a35a21914127fef75ab7bfc5c14e04c0c48613c0d637bcc6df91"
 
 [metadata.files]
 appnope = [
@@ -568,6 +576,8 @@ argon2-cffi = [
     {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"},
     {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"},
     {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"},
+    {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"},
+    {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"},
 ]
 atomicwrites = [
     {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
@@ -581,6 +591,10 @@ backcall = [
     {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
     {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
 ]
+bbcode = [
+    {file = "bbcode-1.1.0-py2.py3-none-any.whl", hash = "sha256:83802f4b40c92426841a98350bd6ff9ea8fdf8f9b37df1968a88c5864fd225fa"},
+    {file = "bbcode-1.1.0.tar.gz", hash = "sha256:eac4fb1d0f6c7ce5c41e4b5c0522562b15a1ac036fb9131adc59e9a28c7dc1d0"},
+]
 certifi = [
     {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
     {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
diff --git a/pyproject.toml b/pyproject.toml
index dcb5d65..11eedc3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "pytaku"
-version = "0.3.35"
+version = "0.4.0"
 description = "Self-hostable web-based manga reader"
 authors = ["Bùi Thành Nhân <hi@imnhan.com>"]
 license = "AGPL-3.0-only"
@@ -26,6 +26,7 @@ gunicorn = "^20.0.4"
 requests = "^2.24.0"
 goodconf = "^1.0.0"
 argon2-cffi = "^20.1.0"
+bbcode = "^1.1.0"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.0.1"
diff --git a/src/mangoapi/mangadex.py b/src/mangoapi/mangadex.py
index c03b350..98d73ea 100644
--- a/src/mangoapi/mangadex.py
+++ b/src/mangoapi/mangadex.py
@@ -2,11 +2,18 @@
 import re
 import time
 
+import bbcode
+
 from mangoapi.base_site import Site, requires_login
 
 MANGAPLUS_GROUP_ID = 9097
 LONG_STRIP_TAG_ID = 36
 
+_bbparser = bbcode.Parser()
+_bbparser.add_simple_formatter(
+    "spoiler", "<details><summary>Spoiler</summary>%(value)s</details>"
+)
+
 
 class Mangadex(Site):
     def get_title(self, title_id):
@@ -30,7 +37,12 @@ def get_title(self, title_id):
             "site": "mangadex",
             "cover_ext": cover_ext,
             "alt_names": manga["altTitles"],
-            "descriptions": html.unescape(manga["description"]).split("\r\n\r\n"),
+            "descriptions": [
+                _bbparser.format(paragraph)
+                for paragraph in html.unescape(manga["description"]).split("\r\n")
+                if paragraph.strip()
+            ],
+            "descriptions_format": "html",
             "is_webtoon": LONG_STRIP_TAG_ID in manga["tags"],
             "chapters": [
                 {
diff --git a/src/mangoapi/mangasee.py b/src/mangoapi/mangasee.py
index c531989..72eba43 100644
--- a/src/mangoapi/mangasee.py
+++ b/src/mangoapi/mangasee.py
@@ -51,6 +51,7 @@ def get_title(self, title_id):
             "chapters": chapters,
             "alt_names": [],
             "descriptions": [desc],
+            "descriptions_format": "text",
         }
 
     def get_chapter(self, title_id, chapter_id):
diff --git a/src/pytaku/database/migrations/latest_schema.sql b/src/pytaku/database/migrations/latest_schema.sql
index c684f1b..37fd01b 100644
--- a/src/pytaku/database/migrations/latest_schema.sql
+++ b/src/pytaku/database/migrations/latest_schema.sql
@@ -9,7 +9,7 @@ CREATE TABLE title (
     chapters text,
     alt_names text,
     descriptions text,
-    updated_at text default (datetime('now')), is_webtoon boolean not null default false,
+    updated_at text default (datetime('now')), is_webtoon boolean not null default false, descriptions_format text not null default 'text',
 
     unique(id, site)
 );
diff --git a/src/pytaku/database/migrations/m0009.sql b/src/pytaku/database/migrations/m0009.sql
new file mode 100644
index 0000000..c2d7f09
--- /dev/null
+++ b/src/pytaku/database/migrations/m0009.sql
@@ -0,0 +1,5 @@
+begin transaction;
+
+alter table title add column descriptions_format text not null default 'text';
+
+commit;
diff --git a/src/pytaku/js-src/routes/title.js b/src/pytaku/js-src/routes/title.js
index bd865ff..47739dc 100644
--- a/src/pytaku/js-src/routes/title.js
+++ b/src/pytaku/js-src/routes/title.js
@@ -131,7 +131,14 @@ function Title(initialVNode) {
                 ),
               ]),
               m("img.title--cover[alt=cover]", { src: title.cover }),
-              title.descriptions.map((desc) => m("p", desc)),
+              m(".title--descriptions", {}, [
+                title.descriptions.map((desc) =>
+                  m(
+                    "p",
+                    title.descriptions_format === "html" ? m.trust(desc) : desc
+                  )
+                ),
+              ]),
               title.chapters
                 ? title.chapters.map((chapter) =>
                     m(Chapter, { site: title.site, titleId: title.id, chapter })
diff --git a/src/pytaku/persistence.py b/src/pytaku/persistence.py
index aa77aca..cf145a7 100644
--- a/src/pytaku/persistence.py
+++ b/src/pytaku/persistence.py
@@ -19,7 +19,8 @@ def save_title(title):
         is_webtoon,
         chapters,
         alt_names,
-        descriptions
+        descriptions,
+        descriptions_format
     ) VALUES (
         :id,
         :name,
@@ -28,7 +29,8 @@ def save_title(title):
         :is_webtoon,
         :chapters,
         :alt_names,
-        :descriptions
+        :descriptions,
+        :descriptions_format
     ) ON CONFLICT (id, site) DO UPDATE SET
         name=excluded.name,
         cover_ext=excluded.cover_ext,
@@ -36,6 +38,7 @@ def save_title(title):
         chapters=excluded.chapters,
         alt_names=excluded.alt_names,
         descriptions=excluded.descriptions,
+        descriptions_format=excluded.descriptions_format,
         updated_at=datetime('now')
     ;
     """,
@@ -48,6 +51,7 @@ def save_title(title):
             "chapters": json.dumps(title["chapters"]),
             "alt_names": json.dumps(title["alt_names"]),
             "descriptions": json.dumps(title["descriptions"]),
+            "descriptions_format": title["descriptions_format"],
         },
     )
 
@@ -55,7 +59,8 @@ def save_title(title):
 def load_title(site, title_id, user_id=None):
     result = run_sql(
         """
-        SELECT id, name, site, cover_ext, is_webtoon, chapters, alt_names, descriptions
+        SELECT id, name, site, cover_ext, is_webtoon, chapters, alt_names, descriptions,
+               descriptions_format
         FROM title
         WHERE id = ?
           AND site = ?;
diff --git a/src/pytaku/static/lookandfeel.css b/src/pytaku/static/lookandfeel.css
index 3e3fd64..bb5e4ae 100644
--- a/src/pytaku/static/lookandfeel.css
+++ b/src/pytaku/static/lookandfeel.css
@@ -17,6 +17,12 @@ :root {
   line-height: 1.2rem;
 }
 
+hr {
+  border: 0;
+  border-top: 1px solid #ddd;
+  margin: 0.5em 0;
+}
+
 h1,
 h2,
 h3,
@@ -126,3 +132,8 @@ a.touch-friendly {
 a.touch-friendly:hover {
   background-color: #eee;
 }
+
+summary {
+  cursor: pointer;
+  user-select: none;
+}
diff --git a/src/pytaku/static/spa.css b/src/pytaku/static/spa.css
index 4e591ac..bed5285 100644
--- a/src/pytaku/static/spa.css
+++ b/src/pytaku/static/spa.css
@@ -347,6 +347,10 @@ .title--details > * {
   margin-right: 0.3rem;
   flex-shrink: 1;
 }
+.title--descriptions {
+  background-color: #eee;
+  padding: 0.5em;
+}
 
 @media (max-width: 399px) {
   .title--details {