diff --git a/.gitignore b/.gitignore index 0ee461e..8038d71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,16 @@ +config/config.py +backend/db/id2igid.db + +# ignore __pycache__ tmp config/session.json config/traceback.json -config/config.py -backend/db/id2igid.db - +## testing files test testfiles test.py - -# for sljh +## for sljh utils/sljh PictureMaker/sljh.py interface/sljh.py diff --git a/PictureMaker/default.py b/PictureMaker/default.py index 4fbc6da..402dc38 100644 --- a/PictureMaker/default.py +++ b/PictureMaker/default.py @@ -1,73 +1,77 @@ -import datetime import os import hashlib import time +from typing import List from playwright.sync_api import sync_playwright -from jinja2 import Template +from jinja2 import Environment, FileSystemLoader -from config.config import TMP +from config.config import TMP, TZINFO +from utils.err import easyExceptionHandler # configuration -TIMEZONE = 8 -IMAGE_WIDTH = 1280 -IMAGE_HEIGHT = 1280 +IMAGE_WIDTH = 1080 +IMAGE_HEIGHT = 1350 +TEMPLATE_DIR = "./PictureMaker/templates" +TARGET_COMMENT_SYSTEM = True # set this to False if your platform not have comment system -def render(text, filename, width=IMAGE_WIDTH, height=IMAGE_HEIGHT, font_size=60, text_color="black", margin=50) -> int: +def render(post_context:dict) -> tuple[list[str], int]: err = 0 browser = None page = None context = None + + fnlist = [] try: with sync_playwright() as p: + # open a browser browser = p.chromium.launch(headless=True) context = browser.new_context() page = context.new_page() - # template - template = Template("{{ text }}") - processed_text = template.render(text=text) - html_content = f""" - - - - - -
{processed_text}
- - -""" - page.set_content(html_content) + # render template + env = Environment(loader=FileSystemLoader(TEMPLATE_DIR)) + template = env.get_template('index.jinja2') + main = { + "id": post_context["id"], + "content": post_context["content"]["text"], + "time": post_context["metadata"]["create_time"].astimezone(tz=TZINFO).strftime("%Y-%m-%d %H:%M:%S") + } + ## (optional) parent article of comment + parent = None + if TARGET_COMMENT_SYSTEM: + from backend.utils import ld_interface + parent_id = post_context["metadata"]["parent"] + if parent_id is not None: + parent_context = ld_interface.inf.get(index=parent_id, media=False) + parent = { + "id": parent_context["id"], + "content": parent_context["content"]["text"] + } + + rendered_html = template.render(main=main, parent=parent) + #print(rendered_html) + + page.set_content(rendered_html) # set window size - page.set_viewport_size({"width": width, "height": height}) - - # screenshot - page.screenshot(path=filename) + page.set_viewport_size({"width": IMAGE_WIDTH, "height": IMAGE_HEIGHT}) + + # screen shot + time.sleep(0.1) + while True: + filename = TMP + hashlib.sha512( str(time.time()).encode() ).hexdigest() + ".jpg" + filename = os.path.abspath(filename) + page.screenshot(path=filename) + fnlist.append(filename) + + result = page.evaluate("scrollPage()") + if result == 1: + break except Exception as e: # exception - print(e) + easyExceptionHandler(e) err = 1 finally: if page: @@ -80,10 +84,10 @@ def render(text, filename, width=IMAGE_WIDTH, height=IMAGE_HEIGHT, font_size=60, try: browser.close() except: pass - return err + return fnlist, err -def gen(context:dict) -> str | None: +def gen(context:dict) -> List[str]: """ Generate a image that has the content of the post in it. @@ -96,16 +100,14 @@ def gen(context:dict) -> str | None: """ # data preparation - content = context["content"]["text"] + # content = context["content"]["text"] # generate image - filename = TMP + hashlib.sha512( str(time.time()).encode() ).hexdigest() + ".jpg" - filename = os.path.abspath(filename) - err = render(content, filename, width=IMAGE_WIDTH, height=IMAGE_HEIGHT) + files, err = render(context) if err: return None - return filename + return files def gen_caption(context:dict) -> str: @@ -119,8 +121,9 @@ def gen_caption(context:dict) -> str: str: caption. """ - caption = f"""[TCIVS Niming #{context["id"]}] -{context["content"]["text"]} + caption = f"""中工匿名#{context["id"]} 🔧 {context["content"]["text"]} + +發布匿名貼文 > niming.tcivs.live """ return caption \ No newline at end of file diff --git a/PictureMaker/templates/index.jinja2 b/PictureMaker/templates/index.jinja2 new file mode 100644 index 0000000..a0f0562 --- /dev/null +++ b/PictureMaker/templates/index.jinja2 @@ -0,0 +1,159 @@ + + + + + + + +
+ + {% if parent is not none %} +
+ Re: #{{ parent.id }} {{ parent.content }} +
+ {% endif %} + + +
{{ main.content }} +
+ + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/TODO b/TODO index d1579f9..9346bc4 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,14 @@ -[ ] 改善Traceback的收集 -[ ] GIF Support +[ ] 處理因為ig媒體畫面比例固定,但是使用者圖片畫面比例不固定導致的問題 + 看要不要幫使用者的媒體填充畫面到正確的比例 [ ] api: ID查IGID,IGID反查ID [ ] api: 返回錯誤處理紀錄 +[ ] Protobuf 重新定義 +[ ] 改善Traceback的收集 +[ ] 隊列本地檔案存儲 + [V] 本地儲存ID對IGID表 +[V] Webp Support +[V] GIF Support +[V] 使用者文章太長,溢出換頁機制 [ ] 測試 \ No newline at end of file diff --git a/backend/processor.py b/backend/processor.py index 34bd3ad..b2f2f32 100644 --- a/backend/processor.py +++ b/backend/processor.py @@ -11,8 +11,12 @@ from backend.utils import ld_interface from backend.utils import ld_picturemaker from backend.utils import fileProcessor -def clean(file_list): +def clean(file_list:list[str]): for f in file_list: + # instagram thumbnail file + if f.endswith(".mp4"): + try: os.remove(f+".jpg") + except: pass try: os.remove(f) except: pass @@ -39,11 +43,11 @@ def upload(aid:int) -> Tuple[str, int]: article["content"]["media"] = [] # 合成文字圖 - proma_file = ld_picturemaker.picture_maker.gen(article) - if proma_file is None: + proma_file:list = ld_picturemaker.picture_maker.gen(article) + if len(proma_file) == 0: # no file returned clean(tmp_path) return "Error while generating proma_file", 1 - tmp_path = [proma_file] + tmp_path + tmp_path = proma_file + tmp_path # 送交 IG 上傳 if not DEBUG: diff --git a/backend/utils/fileProcessor.py b/backend/utils/fileProcessor.py index 7293508..8d36c72 100644 --- a/backend/utils/fileProcessor.py +++ b/backend/utils/fileProcessor.py @@ -14,6 +14,8 @@ from utils.err import easyExceptionHandler register_heif_opener() +# converters +## image def image_conventer(filename:str, binary: bytes) -> int: try: fio = io.BytesIO(binary) @@ -25,7 +27,7 @@ def image_conventer(filename:str, binary: bytes) -> int: easyExceptionHandler(e) return 1 - +## video (and gif) def read_output(pipe, q): """ 用於非阻塞讀取 ffmpeg 的 stdout """ while True: @@ -44,12 +46,27 @@ def video_conventor(filename:str, oriFormat:str, binary:bytes) -> int: f.write(binary) # ffmpeg process - process:subprocess.Popen = ( - ffmpeg - .input(tmpfile, format=oriFormat) - .output(filename, format='mp4') - .run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True) - ) + if oriFormat == "gif": # gif process + process:subprocess.Popen = ( + ffmpeg + .input(tmpfile, format='gif') + .output( + filename, format='mp4', + movflags='faststart', + pix_fmt='yuv420p', + vf='scale=trunc(iw/2)*2:trunc(ih/2)*2,fps=15', + crf=28, + preset='medium' + ) + .run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True) + ) + else: + process:subprocess.Popen = ( + ffmpeg + .input(tmpfile, format=oriFormat) + .output(filename, format='mp4') + .run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True) + ) process.wait() @@ -60,13 +77,13 @@ def video_conventor(filename:str, oriFormat:str, binary:bytes) -> int: except Exception as e: easyExceptionHandler(e) return 1 - +# file_writer def file_writer(filename:str, binary:bytes): with open(filename, "wb") as f: f.write(binary) - +# main def file_saver(ftype:str, binary:bytes) -> Tuple[str, int]: """ ftype -> minetype @@ -87,13 +104,13 @@ def file_saver(ftype:str, binary:bytes) -> Tuple[str, int]: if not ( ftype == "image/jpg" or ftype == "image/webp" or \ ftype == "video/mp4" ): # 轉圖片 - if ftype.startswith("image"): + if ftype.startswith("image") and ftype != "image/gif": opt = os.path.abspath(os.path.join(TMP, filename+".jpg")) err = image_conventer(opt, binary) if err: # 發生錯誤 return "File convert error", 1 # 轉影片 - elif ftype.startswith("video"): + elif ftype.startswith("video") or ftype == "image/gif": opt = os.path.abspath(os.path.join(TMP, filename+".mp4")) err = video_conventor(opt, ext, binary) if err: diff --git a/config/config.py.example b/config/config.py.example index 4fe9e52..7acedfa 100644 --- a/config/config.py.example +++ b/config/config.py.example @@ -1,7 +1,10 @@ +import datetime #################### # General config # #################### TMP = "./tmp/" +TIMEZONE = +8 +TZINFO = datetime.timezone(datetime.timedelta(hours=TIMEZONE)) #################### # Frontend config # @@ -39,6 +42,7 @@ FILE_MINE_TYPE = { "video/mp4": "mp4", "video/quicktime": "mov", "video/hevc": "hevc", + "image/gif": "gif" # convert gif to mp4 } #################### diff --git a/interface/example.py b/interface/example.py index 2da3a1c..c803fdf 100644 --- a/interface/example.py +++ b/interface/example.py @@ -26,32 +26,34 @@ def get(index:int, media:bool=True) -> dict | None: # get data ## fake server in localhost ### where is the api of web application? - res = requests.get("http://localhost:5000/article/%d?media_count=1"%index) + res = requests.get("http://localhost:5000/article/%d?media_count=2"%index) if res.status_code != 200: return None rj = res.json() - media = [] + media_arr = [] # get media if media: for m in rj["media"]: _m = requests.get(m) if _m.status_code == 200: - media.append(io.BytesIO(_m.content)) + media_arr.append(io.BytesIO(_m.content)) # return result = { "id": rj["id"], "metadata": { - "create_time": rj["create_time"], + "create_time": datetime.datetime.fromtimestamp(timestamp=rj["create_time"]), "author": "", "tags": [], - "category": "" + "category": "", + # ext + "parent": rj["parent"] # parent id }, "content": { "text": rj["content"], - "media": media + "media": media_arr } } @@ -65,7 +67,9 @@ an_example_of_context = { "create_time": int | datetime.datetime, "author": str, "tags": list[str], - "category": str + "category": str, + # ext + "parent": int | None # parent id }, "content": { "text": str,