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'[{ frag.desc }]' + 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 %} +