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 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 += '<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>')
@ -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)

View File

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

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.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()

View File

@ -1,5 +1,7 @@
aioflask==0.4.0
flask==2.1.3
aiotieba==3.4.5
Flask-Caching
beautifulsoup4
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 {
--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;
}

View File

@ -44,6 +44,32 @@
</div>
</div>
{% 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>
<footer>
<div><a href="#">RAT Ain't Tieba</a></div>

View File

@ -26,14 +26,41 @@
{% endif %}
</div>
<div class="content">
{{ p['text'] }}
{{ p.contents|translate|safe }}
</div>
<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>
{% endfor %}
</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>
<div><a href="/">RAT Ain't Tieba</a></div>
<div><a href="#">自豪地以 AGPLv3 释出</a></div>