PictureMaker: new template, multi page and page switching ; Backend: support gif and webp files
This commit is contained in:
parent
443a2da0ea
commit
5baac1e696
8 changed files with 283 additions and 84 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,15 +1,16 @@
|
||||||
|
config/config.py
|
||||||
|
backend/db/id2igid.db
|
||||||
|
|
||||||
|
# ignore
|
||||||
__pycache__
|
__pycache__
|
||||||
tmp
|
tmp
|
||||||
config/session.json
|
config/session.json
|
||||||
config/traceback.json
|
config/traceback.json
|
||||||
config/config.py
|
## testing files
|
||||||
backend/db/id2igid.db
|
|
||||||
|
|
||||||
test
|
test
|
||||||
testfiles
|
testfiles
|
||||||
test.py
|
test.py
|
||||||
|
## for sljh
|
||||||
# for sljh
|
|
||||||
utils/sljh
|
utils/sljh
|
||||||
PictureMaker/sljh.py
|
PictureMaker/sljh.py
|
||||||
interface/sljh.py
|
interface/sljh.py
|
||||||
|
|
|
@ -1,73 +1,77 @@
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
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
|
# configuration
|
||||||
TIMEZONE = 8
|
IMAGE_WIDTH = 1080
|
||||||
IMAGE_WIDTH = 1280
|
IMAGE_HEIGHT = 1350
|
||||||
IMAGE_HEIGHT = 1280
|
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
|
err = 0
|
||||||
|
|
||||||
browser = None
|
browser = None
|
||||||
page = None
|
page = None
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
|
fnlist = []
|
||||||
try:
|
try:
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
|
# open a browser
|
||||||
browser = p.chromium.launch(headless=True)
|
browser = p.chromium.launch(headless=True)
|
||||||
context = browser.new_context()
|
context = browser.new_context()
|
||||||
page = context.new_page()
|
page = context.new_page()
|
||||||
|
|
||||||
# template
|
# render template
|
||||||
template = Template("{{ text }}")
|
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
|
||||||
processed_text = template.render(text=text)
|
template = env.get_template('index.jinja2')
|
||||||
html_content = f"""
|
main = {
|
||||||
<html>
|
"id": post_context["id"],
|
||||||
<head>
|
"content": post_context["content"]["text"],
|
||||||
<style>
|
"time": post_context["metadata"]["create_time"].astimezone(tz=TZINFO).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
body {{
|
}
|
||||||
margin: 0;
|
## (optional) parent article of comment
|
||||||
background-size: {width}px {height}px;
|
parent = None
|
||||||
width: {width}px;
|
if TARGET_COMMENT_SYSTEM:
|
||||||
height: {height}px;
|
from backend.utils import ld_interface
|
||||||
display: flex;
|
parent_id = post_context["metadata"]["parent"]
|
||||||
justify-content: center;
|
if parent_id is not None:
|
||||||
align-items: center;
|
parent_context = ld_interface.inf.get(index=parent_id, media=False)
|
||||||
overflow: hidden;
|
parent = {
|
||||||
}}
|
"id": parent_context["id"],
|
||||||
.text-container {{
|
"content": parent_context["content"]["text"]
|
||||||
font-size: {font_size}px;
|
}
|
||||||
color: {text_color};
|
|
||||||
width: calc(100% - {2 * margin}px);
|
rendered_html = template.render(main=main, parent=parent)
|
||||||
text-align: center;
|
#print(rendered_html)
|
||||||
line-height: 1.2;
|
|
||||||
white-space: pre-wrap;
|
page.set_content(rendered_html)
|
||||||
overflow-wrap: break-word;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="text-container">{processed_text}</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
page.set_content(html_content)
|
|
||||||
|
|
||||||
# set window size
|
# set window size
|
||||||
page.set_viewport_size({"width": width, "height": height})
|
page.set_viewport_size({"width": IMAGE_WIDTH, "height": IMAGE_HEIGHT})
|
||||||
|
|
||||||
# screenshot
|
# screen shot
|
||||||
page.screenshot(path=filename)
|
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:
|
except Exception as e:
|
||||||
# exception
|
# exception
|
||||||
print(e)
|
easyExceptionHandler(e)
|
||||||
err = 1
|
err = 1
|
||||||
finally:
|
finally:
|
||||||
if page:
|
if page:
|
||||||
|
@ -80,10 +84,10 @@ def render(text, filename, width=IMAGE_WIDTH, height=IMAGE_HEIGHT, font_size=60,
|
||||||
try: browser.close()
|
try: browser.close()
|
||||||
except: pass
|
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.
|
Generate a image that has the content of the post in it.
|
||||||
|
|
||||||
|
@ -96,16 +100,14 @@ def gen(context:dict) -> str | None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# data preparation
|
# data preparation
|
||||||
content = context["content"]["text"]
|
# content = context["content"]["text"]
|
||||||
|
|
||||||
# generate image
|
# generate image
|
||||||
filename = TMP + hashlib.sha512( str(time.time()).encode() ).hexdigest() + ".jpg"
|
files, err = render(context)
|
||||||
filename = os.path.abspath(filename)
|
|
||||||
err = render(content, filename, width=IMAGE_WIDTH, height=IMAGE_HEIGHT)
|
|
||||||
if err:
|
if err:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return filename
|
return files
|
||||||
|
|
||||||
|
|
||||||
def gen_caption(context:dict) -> str:
|
def gen_caption(context:dict) -> str:
|
||||||
|
@ -119,8 +121,9 @@ def gen_caption(context:dict) -> str:
|
||||||
str: caption.
|
str: caption.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
caption = f"""[TCIVS Niming #{context["id"]}]
|
caption = f"""中工匿名#{context["id"]} 🔧 {context["content"]["text"]}
|
||||||
{context["content"]["text"]}
|
|
||||||
|
發布匿名貼文 > niming.tcivs.live
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return caption
|
return caption
|
159
PictureMaker/templates/index.jinja2
Normal file
159
PictureMaker/templates/index.jinja2
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
/*align-items: center;
|
||||||
|
justify-content: center;*/
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 1080px;
|
||||||
|
max-height: 1350px;
|
||||||
|
height: 1350px;
|
||||||
|
display: grid;
|
||||||
|
{% if parent is not none %}
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
{% else %}
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
{% endif %}
|
||||||
|
background-color: #fff;
|
||||||
|
/*margin: 20px;*/
|
||||||
|
/*padding: 40px;*/
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reply info */
|
||||||
|
.replybox {
|
||||||
|
background-color: lightblue;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 40px 40px 0px 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content */
|
||||||
|
.contentbox {
|
||||||
|
/*background-color: blue;*/
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 45px;
|
||||||
|
{% if parent is not none %}
|
||||||
|
margin: 20px 40px 0px 40px;
|
||||||
|
{% else %}
|
||||||
|
margin: 40px 40px 0px 40px;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
/* hide scroll bar */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content - hide scroll bar */
|
||||||
|
.contentbox::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* center if not overflow */
|
||||||
|
.container .center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* metadata */
|
||||||
|
.metadatabox {
|
||||||
|
margin: 0px 40px 40px 40px;
|
||||||
|
color: grey;
|
||||||
|
font-size: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
vertical-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.metadata-grid .left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.metadata-grid .center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.metadata-grid .right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- reply info -->
|
||||||
|
{% if parent is not none %}
|
||||||
|
<div class="replybox">
|
||||||
|
Re: #{{ parent.id }} {{ parent.content }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- main content -->
|
||||||
|
<div class="contentbox"><a class="content">{{ main.content }}
|
||||||
|
</a></div>
|
||||||
|
|
||||||
|
<!-- metadata -->
|
||||||
|
<div class="metadatabox">
|
||||||
|
<hr/>
|
||||||
|
<div class="metadata-grid">
|
||||||
|
<!-- <div class="left">IG: niming.tcivs</div> -->
|
||||||
|
<div class="left">匿名中工#{{ main.id }}</div>
|
||||||
|
<div class="center">niming.tcivs.live</div>
|
||||||
|
<div class="right">{{ main.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const contentbox = document.getElementsByClassName("contentbox")[0];
|
||||||
|
var page = 1;
|
||||||
|
|
||||||
|
// check overflow
|
||||||
|
function checkOverflow() {
|
||||||
|
if (contentbox.scrollHeight <= contentbox.clientHeight) {
|
||||||
|
contentbox.classList.add("center");
|
||||||
|
} else {
|
||||||
|
contentbox.classList.remove("center");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll page
|
||||||
|
function scrollPage() {
|
||||||
|
let total_page = Math.ceil(contentbox.scrollHeight / contentbox.clientHeight);
|
||||||
|
if (total_page > 1 && page < total_page) {
|
||||||
|
contentbox.scrollTo(0, contentbox.scrollTop + contentbox.clientHeight - 45);
|
||||||
|
page++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
window.addEventListener("load", checkOverflow);
|
||||||
|
// Optional: recheck on resize
|
||||||
|
// window.addEventListener("resize", checkOverflow);
|
||||||
|
</script>
|
11
TODO
11
TODO
|
@ -1,7 +1,14 @@
|
||||||
[ ] 改善Traceback的收集
|
[ ] 處理因為ig媒體畫面比例固定,但是使用者圖片畫面比例不固定導致的問題
|
||||||
[ ] GIF Support
|
看要不要幫使用者的媒體填充畫面到正確的比例
|
||||||
[ ] api: ID查IGID,IGID反查ID
|
[ ] api: ID查IGID,IGID反查ID
|
||||||
[ ] api: 返回錯誤處理紀錄
|
[ ] api: 返回錯誤處理紀錄
|
||||||
|
[ ] Protobuf 重新定義
|
||||||
|
[ ] 改善Traceback的收集
|
||||||
|
[ ] 隊列本地檔案存儲
|
||||||
|
|
||||||
[V] 本地儲存ID對IGID表
|
[V] 本地儲存ID對IGID表
|
||||||
|
[V] Webp Support
|
||||||
|
[V] GIF Support
|
||||||
|
[V] 使用者文章太長,溢出換頁機制
|
||||||
|
|
||||||
[ ] 測試
|
[ ] 測試
|
|
@ -11,8 +11,12 @@ from backend.utils import ld_interface
|
||||||
from backend.utils import ld_picturemaker
|
from backend.utils import ld_picturemaker
|
||||||
from backend.utils import fileProcessor
|
from backend.utils import fileProcessor
|
||||||
|
|
||||||
def clean(file_list):
|
def clean(file_list:list[str]):
|
||||||
for f in file_list:
|
for f in file_list:
|
||||||
|
# instagram thumbnail file
|
||||||
|
if f.endswith(".mp4"):
|
||||||
|
try: os.remove(f+".jpg")
|
||||||
|
except: pass
|
||||||
try: os.remove(f)
|
try: os.remove(f)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
@ -39,11 +43,11 @@ def upload(aid:int) -> Tuple[str, int]:
|
||||||
article["content"]["media"] = []
|
article["content"]["media"] = []
|
||||||
|
|
||||||
# 合成文字圖
|
# 合成文字圖
|
||||||
proma_file = ld_picturemaker.picture_maker.gen(article)
|
proma_file:list = ld_picturemaker.picture_maker.gen(article)
|
||||||
if proma_file is None:
|
if len(proma_file) == 0: # no file returned
|
||||||
clean(tmp_path)
|
clean(tmp_path)
|
||||||
return "Error while generating proma_file", 1
|
return "Error while generating proma_file", 1
|
||||||
tmp_path = [proma_file] + tmp_path
|
tmp_path = proma_file + tmp_path
|
||||||
|
|
||||||
# 送交 IG 上傳
|
# 送交 IG 上傳
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
|
|
|
@ -14,6 +14,8 @@ from utils.err import easyExceptionHandler
|
||||||
|
|
||||||
register_heif_opener()
|
register_heif_opener()
|
||||||
|
|
||||||
|
# converters
|
||||||
|
## image
|
||||||
def image_conventer(filename:str, binary: bytes) -> int:
|
def image_conventer(filename:str, binary: bytes) -> int:
|
||||||
try:
|
try:
|
||||||
fio = io.BytesIO(binary)
|
fio = io.BytesIO(binary)
|
||||||
|
@ -25,7 +27,7 @@ def image_conventer(filename:str, binary: bytes) -> int:
|
||||||
easyExceptionHandler(e)
|
easyExceptionHandler(e)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
## video (and gif)
|
||||||
def read_output(pipe, q):
|
def read_output(pipe, q):
|
||||||
""" 用於非阻塞讀取 ffmpeg 的 stdout """
|
""" 用於非阻塞讀取 ffmpeg 的 stdout """
|
||||||
while True:
|
while True:
|
||||||
|
@ -44,12 +46,27 @@ def video_conventor(filename:str, oriFormat:str, binary:bytes) -> int:
|
||||||
f.write(binary)
|
f.write(binary)
|
||||||
|
|
||||||
# ffmpeg process
|
# ffmpeg process
|
||||||
process:subprocess.Popen = (
|
if oriFormat == "gif": # gif process
|
||||||
ffmpeg
|
process:subprocess.Popen = (
|
||||||
.input(tmpfile, format=oriFormat)
|
ffmpeg
|
||||||
.output(filename, format='mp4')
|
.input(tmpfile, format='gif')
|
||||||
.run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True)
|
.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()
|
process.wait()
|
||||||
|
|
||||||
|
@ -60,13 +77,13 @@ def video_conventor(filename:str, oriFormat:str, binary:bytes) -> int:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
easyExceptionHandler(e)
|
easyExceptionHandler(e)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
# file_writer
|
||||||
def file_writer(filename:str, binary:bytes):
|
def file_writer(filename:str, binary:bytes):
|
||||||
with open(filename, "wb") as f:
|
with open(filename, "wb") as f:
|
||||||
f.write(binary)
|
f.write(binary)
|
||||||
|
|
||||||
|
# main
|
||||||
def file_saver(ftype:str, binary:bytes) -> Tuple[str, int]:
|
def file_saver(ftype:str, binary:bytes) -> Tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
ftype -> minetype
|
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 \
|
if not ( ftype == "image/jpg" or ftype == "image/webp" or \
|
||||||
ftype == "video/mp4" ):
|
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"))
|
opt = os.path.abspath(os.path.join(TMP, filename+".jpg"))
|
||||||
err = image_conventer(opt, binary)
|
err = image_conventer(opt, binary)
|
||||||
if err: # 發生錯誤
|
if err: # 發生錯誤
|
||||||
return "File convert error", 1
|
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"))
|
opt = os.path.abspath(os.path.join(TMP, filename+".mp4"))
|
||||||
err = video_conventor(opt, ext, binary)
|
err = video_conventor(opt, ext, binary)
|
||||||
if err:
|
if err:
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import datetime
|
||||||
####################
|
####################
|
||||||
# General config #
|
# General config #
|
||||||
####################
|
####################
|
||||||
TMP = "./tmp/"
|
TMP = "./tmp/"
|
||||||
|
TIMEZONE = +8
|
||||||
|
TZINFO = datetime.timezone(datetime.timedelta(hours=TIMEZONE))
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Frontend config #
|
# Frontend config #
|
||||||
|
@ -39,6 +42,7 @@ FILE_MINE_TYPE = {
|
||||||
"video/mp4": "mp4",
|
"video/mp4": "mp4",
|
||||||
"video/quicktime": "mov",
|
"video/quicktime": "mov",
|
||||||
"video/hevc": "hevc",
|
"video/hevc": "hevc",
|
||||||
|
"image/gif": "gif" # convert gif to mp4
|
||||||
}
|
}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -26,32 +26,34 @@ def get(index:int, media:bool=True) -> dict | None:
|
||||||
# get data
|
# get data
|
||||||
## fake server in localhost
|
## fake server in localhost
|
||||||
### where is the api of web application?
|
### 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:
|
if res.status_code != 200:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
rj = res.json()
|
rj = res.json()
|
||||||
media = []
|
media_arr = []
|
||||||
|
|
||||||
# get media
|
# get media
|
||||||
if media:
|
if media:
|
||||||
for m in rj["media"]:
|
for m in rj["media"]:
|
||||||
_m = requests.get(m)
|
_m = requests.get(m)
|
||||||
if _m.status_code == 200:
|
if _m.status_code == 200:
|
||||||
media.append(io.BytesIO(_m.content))
|
media_arr.append(io.BytesIO(_m.content))
|
||||||
|
|
||||||
# return
|
# return
|
||||||
result = {
|
result = {
|
||||||
"id": rj["id"],
|
"id": rj["id"],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"create_time": rj["create_time"],
|
"create_time": datetime.datetime.fromtimestamp(timestamp=rj["create_time"]),
|
||||||
"author": "",
|
"author": "",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"category": ""
|
"category": "",
|
||||||
|
# ext
|
||||||
|
"parent": rj["parent"] # parent id
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"text": rj["content"],
|
"text": rj["content"],
|
||||||
"media": media
|
"media": media_arr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +67,9 @@ an_example_of_context = {
|
||||||
"create_time": int | datetime.datetime,
|
"create_time": int | datetime.datetime,
|
||||||
"author": str,
|
"author": str,
|
||||||
"tags": list[str],
|
"tags": list[str],
|
||||||
"category": str
|
"category": str,
|
||||||
|
# ext
|
||||||
|
"parent": int | None # parent id
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"text": str,
|
"text": str,
|
||||||
|
|
Loading…
Add table
Reference in a new issue