This commit is contained in:
John Xina 2023-07-11 22:23:48 +08:00
parent aafa24ddc7
commit f17e332ee4
8 changed files with 212 additions and 49 deletions

42
app.py
View File

@ -1,12 +1,16 @@
import asyncio import asyncio
import aiotieba 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 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): def _jinja2_filter_trim(text):
return text[:78] + '……' if len(text) > 78 else 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 += '<p>' + str(escape(subfrag)) + '</p>'
elif isinstance(frag, FragImage_p):
htmlfmt += \
f'<a target="_blank" href="/proxy/pic/{ extract_image_name(frag.origin_src) }">' \
f'<img width="{ frag.show_width}" height="{ frag.show_height }" '\
f'src="/proxy/pic/{ extract_image_name(frag.src) }"></a>'
elif isinstance(frag, FragEmoji_p):
clear_leading = False
if htmlfmt.endswith('</p>'):
clear_leading = True
htmlfmt = htmlfmt.rstrip('</p>')
htmlfmt += f'<img class="emoticons" alt="[{ frag.desc }]" src="/static/emoticons/{ quote_plus(frag.desc) }.png">'
if clear_leading:
htmlfmt += '</p>'
return htmlfmt
###################################################################### ######################################################################
@app.route('/p/<tid>') @app.route('/p/<tid>')
@ -49,8 +79,8 @@ async def thread_view(tid):
# Default to 15 posts per page, confirm to tieba.baidu.com # Default to 15 posts per page, confirm to tieba.baidu.com
thread_info = await tieba.get_posts(tid, rn=15, pn=pn) thread_info = await tieba.get_posts(tid, rn=15, pn=pn)
for fragment in thread_info[0].contents: for post in thread_info:
print(fragment) print(post.comments)
return await render_template('thread.html', info=thread_info) 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) pn = int(request.args.get('pn') or 1)
async with aiotieba.Client() as tieba: 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)) tieba.get_threads(fname, rn=50, pn=pn))
return await render_template('bar.html', info=forum_info, threads=threads) return await render_template('bar.html', info=forum_info, threads=threads)

View File

@ -4,7 +4,14 @@ import requests
import bs4 import bs4
import re 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. """Get the tiebat avatar for the forum name.
:param tname: the name of the target forum. :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') soup = bs4.BeautifulSoup(res.text, 'html.parser')
elems = soup.select('#forum-card-head') elems = soup.select('#forum-card-head')
match = re.search(r'/(\w+)\.jpg', elems[0]['src']) info['avatar'] = extract_image_name(elems[0]['src'])
info['avatar'] = match.group(1) + '.jpg'
footer = soup.select('.th_footer_l')[0] footer = soup.select('.th_footer_l')[0]
stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False) stat_elems = footer.findAll('span', {'class': 'red_text'}, recursive=False)

14
main.py
View File

@ -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.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
from twisted.web.resource import Resource from twisted.web.resource import Resource
from twisted.web import proxy, server from twisted.web import proxy, server
from twisted.web.static import File
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, utils from twisted.internet import reactor, utils
@ -139,18 +140,25 @@ class ReverseProxyResource(Resource):
def twisted_start(): def twisted_start():
flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'') flask_res = proxy.ReverseProxyResource('127.0.0.1', 5000, b'')
flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy')) 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) site = server.Site(flask_res)
reactor.listenTCP(5001, site) reactor.listenTCP(flask_port-1, site)
reactor.run() reactor.run()
# To start this function for testing: python -c 'import main; main.flask_start()' # To start this function for testing: python -c 'import main; main.flask_start()'
def flask_start(): def flask_start():
app.run(port=5000+1) app.run()
# If we're executed directly, also start the flask daemon. # If we're executed directly, also start the flask daemon.
if __name__ == '__main__': 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 = multiprocessing.Process(target=flask_start)
flask_task.daemon = True # Exit the child if the parent was killed :-( flask_task.daemon = True # Exit the child if the parent was killed :-(
flask_task.start() flask_task.start()
twisted_start()

View File

@ -1,5 +1,7 @@
aioflask==0.4.0 aioflask==0.4.0
flask==2.1.3 flask==2.1.3
aiotieba==3.4.5 aiotieba==3.4.5
Flask-Caching
beautifulsoup4 beautifulsoup4
requests requests
twisted

18
shared.py Normal file
View File

@ -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)

View File

@ -1,19 +1,25 @@
.emoticons {
max-width: 5% !important;
}
/* global styling */
:root { :root {
--bg-color: #eeeecc; --bg-color: #eeeecc;
--fg-color: #ffffdd; --fg-color: #ffffdd;
--replies-color: #f0f0d0; --replies-color: #f0f0d0;
--text-color: #663333; --text-color: #663333;
--border-color: #66333388;
--primary-color: #0066cc; --primary-color: #0066cc;
--important-color: #ff0000; --important-color: #ff0000;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--bg-color: #000033; --bg-color: #000033;
--fg-color: #202044; --fg-color: #202044;
--replies-color: #16163a; --replies-color: #16163a;
--text-color: #cccccc; --text-color: #cccccc;
--border-color: #cccccc44;
--primary-color: #6699ff; --primary-color: #6699ff;
--important-color: #ff0000; --important-color: #ff0000;
} }
@ -27,6 +33,15 @@ body {
font-family: sans-serif; font-family: sans-serif;
} }
footer {
display: flex;
gap: 2rem;
padding: 1rem;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
a[href] { a[href] {
color: var(--primary-color); color: var(--primary-color);
text-decoration: none; text-decoration: none;
@ -40,7 +55,24 @@ img {
max-width: 100%; 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); background-color: var(--fg-color);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -50,38 +82,29 @@ img {
align-items: center; 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 { .list {
background-color: var(--fg-color); background-color: var(--fg-color);
margin-bottom: 1rem; 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 { .post {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0 1rem; gap: 0 1rem;
border-bottom: 1px solid var(--text-color); border-bottom: 1px solid var(--border-color);
padding: 1rem; padding: 1rem;
} }
@ -120,6 +143,39 @@ img {
float: right; 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 { .thread {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
@ -155,12 +211,3 @@ img {
padding-left: .3rem; padding-left: .3rem;
color: var(--text-color); color: var(--text-color);
} }
footer {
display: flex;
gap: 2rem;
padding: 1rem;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}

View File

@ -44,6 +44,32 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="paginator">
{% if threads.page.current_page > 1 %}
<a href="/f?kw={{ info['name'] }}">首页</a>
{% endif %}
{% for i in range(5) %}
{% set np = threads.page.current_page - 5 + i %}
{% if np > 0 %}
<a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
{% endif %}
{% endfor %}
<a>{{ threads.page.current_page }}</a>
{% for i in range(5) %}
{% set np = threads.page.current_page + 1 + i %}
{% if np <= threads.page.total_page %}
<a href="/f?kw={{ info['name'] }}&pn={{ np }}">{{ np }}</a>
{% endif %}
{% endfor %}
{% if threads.page.current_page < threads.page.total_page %}
<a href="/f?kw={{ info['name'] }}&pn={{ threads.page.total_page }}">尾页</a>
{% endif %}
</div>
</div> </div>
<footer> <footer>
<div><a href="#">RAT Ain't Tieba</a></div> <div><a href="#">RAT Ain't Tieba</a></div>

View File

@ -26,14 +26,41 @@
{% endif %} {% endif %}
</div> </div>
<div class="content"> <div class="content">
{{ p['text'] }} {{ p.contents|translate|safe }}
</div> </div>
<small class="date">{{ p.create_time|date }}</small> <small class="date">{{ p.create_time|date }}</small>
<small class="permalink"><a href="#1">{{ p.floor }}</a></small> <small class="permalink"><a href="#{{ p.floor }}">{{ p.floor }}</a></small>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="paginator">
{% if info.page.current_page > 1 %}
<a href="/p/{{ info.thread.tid }}">首页</a>
{% endif %}
{% for i in range(5) %}
{% set np = info.page.current_page - 5 + i %}
{% if np > 0 %}
<a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
{% endif %}
{% endfor %}
<a>{{ info.page.current_page }}</a>
{% for i in range(5) %}
{% set np = info.page.current_page + 1 + i %}
{% if np <= info.page.total_page %}
<a href="/p/{{ info.thread.tid }}?pn={{ np }}">{{ np }}</a>
{% endif %}
{% endfor %}
{% if info.page.current_page < info.page.total_page %}
<a href="/p/{{ info.thread.tid }}?pn={{ info.page.total_page }}">尾页</a>
{% endif %}
</div>
<footer> <footer>
<div><a href="/">RAT Ain't Tieba</a></div> <div><a href="/">RAT Ain't Tieba</a></div>
<div><a href="#">自豪地以 AGPLv3 释出</a></div> <div><a href="#">自豪地以 AGPLv3 释出</a></div>