From f17e332ee483fbdde92337794e7029f4c39c2162 Mon Sep 17 00:00:00 2001
From: John Xina
Date: Tue, 11 Jul 2023 22:23:48 +0800
Subject: [PATCH] minimal
---
app.py | 42 ++++++++++++---
extra.py | 13 +++--
main.py | 14 +++--
requirements.txt | 2 +
shared.py | 18 +++++++
static/css/main.css | 115 +++++++++++++++++++++++++++++-------------
templates/bar.html | 26 ++++++++++
templates/thread.html | 31 +++++++++++-
8 files changed, 212 insertions(+), 49 deletions(-)
create mode 100644 shared.py
diff --git a/app.py b/app.py
index 30e7533..1e3a7aa 100644
--- a/app.py
+++ b/app.py
@@ -1,12 +1,16 @@
import asyncio
import aiotieba
-from aioflask import Flask, render_template, request
+from aioflask import render_template, request, escape
+from flask_caching import Cache
+from urllib.parse import quote_plus
from datetime import datetime
-from extra import *
+from aiotieba.api.get_posts._classdef import *
+from aiotieba.api._classdef.contents import *
-app = Flask(__name__)
+from shared import *
+from extra import *
######################################################################
@@ -38,6 +42,32 @@ def _jinja2_filter_intsep(i):
def _jinja2_filter_trim(text):
return text[:78] + '……' if len(text) > 78 else text
+# Format fragments to its equiviant HTML.
+@app.template_filter('translate')
+def _jinja2_filter_translate(frags):
+ htmlfmt = ''
+
+ for frag in frags:
+ if isinstance(frag, FragText):
+ subfrags = frag.text.split('\n')
+ for subfrag in subfrags:
+ htmlfmt += '' + str(escape(subfrag)) + '
'
+ elif isinstance(frag, FragImage_p):
+ htmlfmt += \
+ f'' \
+ f''
+ elif isinstance(frag, FragEmoji_p):
+ clear_leading = False
+ if htmlfmt.endswith('
'):
+ clear_leading = True
+ htmlfmt = htmlfmt.rstrip('')
+ htmlfmt += f''
+ if clear_leading:
+ htmlfmt += ''
+
+ return htmlfmt
+
######################################################################
@app.route('/p/')
@@ -49,8 +79,8 @@ async def thread_view(tid):
# Default to 15 posts per page, confirm to tieba.baidu.com
thread_info = await tieba.get_posts(tid, rn=15, pn=pn)
- for fragment in thread_info[0].contents:
- print(fragment)
+ for post in thread_info:
+ print(post.comments)
return await render_template('thread.html', info=thread_info)
@@ -60,7 +90,7 @@ async def forum_view():
pn = int(request.args.get('pn') or 1)
async with aiotieba.Client() as tieba:
- forum_info, threads = await asyncio.gather(find_tieba_info(fname),
+ forum_info, threads = await asyncio.gather(awaitify(find_tieba_info)(fname),
tieba.get_threads(fname, rn=50, pn=pn))
return await render_template('bar.html', info=forum_info, threads=threads)
diff --git a/extra.py b/extra.py
index ad8ccf6..2bfa4e1 100644
--- a/extra.py
+++ b/extra.py
@@ -4,7 +4,14 @@ import requests
import bs4
import re
-async def find_tieba_info(tname):
+from shared import *
+
+def extract_image_name(url):
+ match = re.search(r'/(\w+)\.jpg', url)
+ return match.group(1) + '.jpg'
+
+@cache.cached(timeout=60, key_prefix='tieba_info')
+def find_tieba_info(tname):
"""Get the tiebat avatar for the forum name.
:param tname: the name of the target forum.
@@ -17,9 +24,7 @@ async def find_tieba_info(tname):
soup = bs4.BeautifulSoup(res.text, 'html.parser')
elems = soup.select('#forum-card-head')
- match = re.search(r'/(\w+)\.jpg', elems[0]['src'])
-
- info['avatar'] = match.group(1) + '.jpg'
+ info['avatar'] = extract_image_name(elems[0]['src'])
footer = soup.select('.th_footer_l')[0]
stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False)
diff --git a/main.py b/main.py
index 2bf0811..a02b03c 100644
--- a/main.py
+++ b/main.py
@@ -6,6 +6,7 @@ from urllib.parse import quote as urlquote, urlparse, urlunparse
from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
from twisted.web.resource import Resource
from twisted.web import proxy, server
+from twisted.web.static import File
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, utils
@@ -139,18 +140,25 @@ class ReverseProxyResource(Resource):
def twisted_start():
flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
+ flask_res.putChild(b'static', File('static'))
+ flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+
site = server.Site(flask_res)
- reactor.listenTCP(5001, site)
+ reactor.listenTCP(flask_port-1, site)
reactor.run()
# To start this function for testing: python -c 'import main; main.flask_start()'
def flask_start():
- app.run(port=5000+1)
+ app.run()
# If we're executed directly, also start the flask daemon.
if __name__ == '__main__':
+ flask_port = int(app.config['SERVER_NAME'].split(':')[1])
+ print(f' *** SERVER IS RUNNING ON PORT {flask_port-1} ***')
+
+ twisted_start()
+
flask_task = multiprocessing.Process(target=flask_start)
flask_task.daemon = True # Exit the child if the parent was killed :-(
flask_task.start()
- twisted_start()
diff --git a/requirements.txt b/requirements.txt
index 72c0821..d6ac9b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,7 @@
aioflask==0.4.0
flask==2.1.3
aiotieba==3.4.5
+Flask-Caching
beautifulsoup4
requests
+twisted
diff --git a/shared.py b/shared.py
new file mode 100644
index 0000000..87adbef
--- /dev/null
+++ b/shared.py
@@ -0,0 +1,18 @@
+from aioflask import Flask
+from flask_caching import Cache
+
+from functools import wraps
+
+def awaitify(sync_func):
+ """Wrap a synchronous callable to allow ``await``'ing it"""
+ @wraps(sync_func)
+ async def async_func(*args, **kwargs):
+ return sync_func(*args, **kwargs)
+ return async_func
+
+app = Flask(__name__)
+
+app.config['SERVER_NAME'] = ':6666'
+
+app.config['CACHE_TYPE'] = 'SimpleCache'
+cache = Cache(app)
diff --git a/static/css/main.css b/static/css/main.css
index 4c87201..a85c19a 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1,19 +1,25 @@
+.emoticons {
+ max-width: 5% !important;
+}
+
+/* global styling */
:root {
--bg-color: #eeeecc;
--fg-color: #ffffdd;
--replies-color: #f0f0d0;
--text-color: #663333;
+ --border-color: #66333388;
--primary-color: #0066cc;
--important-color: #ff0000;
}
-
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000033;
--fg-color: #202044;
--replies-color: #16163a;
--text-color: #cccccc;
+ --border-color: #cccccc44;
--primary-color: #6699ff;
--important-color: #ff0000;
}
@@ -27,6 +33,15 @@ body {
font-family: sans-serif;
}
+footer {
+ display: flex;
+ gap: 2rem;
+ padding: 1rem;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
a[href] {
color: var(--primary-color);
text-decoration: none;
@@ -40,7 +55,24 @@ img {
max-width: 100%;
}
-.bar-nav, .thread-nav {
+.paginator {
+ padding: 1rem;
+ gap: .3rem;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.paginator a {
+ flex: 0 0 auto;
+ height: 1rem;
+ line-height: 1rem;
+ padding: .5rem;
+ text-align: center;
+ background-color: var(--replies-color);
+}
+
+header {
background-color: var(--fg-color);
display: flex;
flex-wrap: wrap;
@@ -50,38 +82,29 @@ img {
align-items: center;
}
-.thread-nav > .title {
- font-size: 1.2rem;
- flex: 1 0 70%;
-}
-
-.thread-nav > .from {
- font-size: 1.2rem;
-}
-
-.bar-nav > img {
- width: 5rem;
- height: 5rem;
-}
-
-.bar-nav .title {
- font-size: 1.5rem;
-}
-
-.bar-nav .stats small {
- margin-right: .5rem;
-}
-
.list {
background-color: var(--fg-color);
margin-bottom: 1rem;
}
+/* thread.html: nav bar */
+
+.thread-nav .title {
+ font-size: 1.2rem;
+ flex: 1 0 70%;
+}
+
+.thread-nav .from {
+ font-size: 1.2rem;
+}
+
+/* thread.html: user post */
+
.post {
display: flex;
flex-wrap: wrap;
gap: 0 1rem;
- border-bottom: 1px solid var(--text-color);
+ border-bottom: 1px solid var(--border-color);
padding: 1rem;
}
@@ -120,6 +143,39 @@ img {
float: right;
}
+/* thread.html: replies to a user post */
+
+.post .replies {
+ background-color: var(--replies-color);
+ margin-top: 1rem;
+}
+
+.post .replies .post {
+ border-bottom: none;
+}
+
+.post .replies .post .avatar {
+ width: 3rem;
+ height: 3rem;
+}
+
+/* bar.html: nav bar */
+
+.bar-nav img {
+ width: 5rem;
+ height: 5rem;
+}
+
+.bar-nav .title {
+ font-size: 1.5rem;
+}
+
+.bar-nav .stats small {
+ margin-right: .5rem;
+}
+
+/* bar.html: thread list */
+
.thread {
display: flex;
gap: 1rem;
@@ -155,12 +211,3 @@ img {
padding-left: .3rem;
color: var(--text-color);
}
-
-footer {
- display: flex;
- gap: 2rem;
- padding: 1rem;
- justify-content: center;
- align-items: center;
- flex-wrap: wrap;
-}
diff --git a/templates/bar.html b/templates/bar.html
index c2f0ae7..73c38c2 100644
--- a/templates/bar.html
+++ b/templates/bar.html
@@ -44,6 +44,32 @@
{% endfor %}
+
+
+ {% if threads.page.current_page > 1 %}
+
首页
+ {% endif %}
+
+ {% for i in range(5) %}
+ {% set np = threads.page.current_page - 5 + i %}
+ {% if np > 0 %}
+
{{ np }}
+ {% endif %}
+ {% endfor %}
+
+
{{ threads.page.current_page }}
+
+ {% for i in range(5) %}
+ {% set np = threads.page.current_page + 1 + i %}
+ {% if np <= threads.page.total_page %}
+
{{ np }}
+ {% endif %}
+ {% endfor %}
+
+ {% if threads.page.current_page < threads.page.total_page %}
+
尾页
+ {% endif %}
+