first commit

This commit is contained in:
yumoqing 2025-06-24 11:45:33 +08:00
commit e838d19d9f
121 changed files with 1994 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# media

Binary file not shown.

Binary file not shown.

30
app/add_imageinfo.py Normal file
View File

@ -0,0 +1,30 @@
import os, sys
import asyncio
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
from ahserver.filestorage import FileStorage
from loadmedia import save_image_info
async def main():
workdir = os.path.join(os.environ.get('HOME'), 'py', 'media')
config = getConfig(workdir, {'workdir':workdir})
imgs = []
fs = FileStorage()
db = DBPools(config.databases)
async with db.sqlorContext('mediadb') as sor:
sql = """select a.* from media a left join imageinfo b
on a.id = b.id
where a.mtype = 'image'
and b.id is NULL
order by ownerid
"""
imgs = await sor.sqlExe(sql, {})
cnt = len(imgs)
for i, img in enumerate(imgs):
async with db.sqlorContext('mediadb') as sor:
print(f'handle {img.mlocation} {i}/{cnt}')
path = fs.realPath(img.mlocation)
await save_image_info(sor, img.ownerid, img.id, path, img.ownerid)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

59
app/ext.py Normal file
View File

@ -0,0 +1,59 @@
import json
from traceback import print_exc
from ahserver.serverenv import ServerEnv
from ahserver.filestorage import FileStorage
from sqlor.dbpools import DBPools
from appPublic.timeUtils import curDateString, timestampstr
from appPublic.dictObject import DictObject
from appPublic.log import info, debug, warning, error, exception, critical
from appPublic.uniqueID import getID
from appPublic.registerfunction import RegisterFunction, rfexe
def UiWindow(title, icon, content, cheight=10, cwidth=15):
return {
"widgettype":"PopupWindow",
"options":{
"author":"cc",
"cwidth":cwidth,
"cheight":cheight,
"title":title,
"content":content,
"icon":icon or entire_url('/bricks/imgs/app.png'),
"movable":True,
"auto_open":True
}
}
def UiError(title="出错", message="出错啦", timeout=5):
return {
"widgettype":"Error",
"options":{
"author":"tr",
"timeout":timeout,
"cwidth":15,
"cheight":10,
"title":title,
"message":message
}
}
def UiMessage(title="消息", message="后台消息", timeout=5):
return {
"widgettype":"Message",
"options":{
"author":"tr",
"timeout":timeout,
"cwidth":15,
"cheight":10,
"title":title,
"message":message
}
}
def init_ui_ext():
g = ServerEnv()
g.UiError = UiError
g.UiMessage = UiMessage
g.UiWindow = UiWindow

47
app/imageface.py Normal file
View File

@ -0,0 +1,47 @@
from appPublic.worker import awaitify
from appPublic.log import debug, exception
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
from ahserver.filestorage import FileStorage
import face_recognition
from PIL import Image
async def create_knownface(imgid, logo_url, name):
db = DBPools()
dbname = 'mediadb'
async with db.sqlorContext(dbname) as sor:
return await _create_knownface(sor, imgid, logo_url, name)
e = Exception(f'{imgid=}, {logo_url=}, {name=}:create_knownface() error')
exception(f'{e=}')
raise e
async def _create_knownface(sor, imgid, logo_url, name):
fs = FileStorage()
imgpath = fs.realPath(logo_url)
imgobj = await awaitify(face_recognition.load_image_file)(imgpath)
infos = await awaitify(face_recognition.face_encodings)(imgobj)
locs = await awaitify(face_recognition.face_locations)(imgobj)
if len(locs) > 1:
e = Exception(f'{imgpath} found more than one face')
exception(f'{e=}')
raise e
if len(locs) == 0:
e = Exception(f'{imgpath} found not face')
exception(f'{e=}')
raise e
l = locs[0]
faceinfo = json.dumps({
'feature':infos[0],
"face_top":top,
"face_bottom":bottom,
"face_left":left,
"face_right":right
})
ns = {
"id":getID(),
"mediaid":imgid,
"faceinfo":faceinfo,
"personname":name
}
await sor.C('photofaces', ns)

89
app/imageinfo.py Normal file
View File

@ -0,0 +1,89 @@
from PIL import Image, ExifTags
import exifread
import pyheif
from pillow_heif import register_heif_opener
from findperson.imageface import ImageFaces
register_heif_opener()
ifs = None
def get_imagefaces(imgfile, imgid, userid):
global ifs
if ifs is None:
ifs = ImageFaces()
info = ifs.save_faces(userid, imgfile, imgid=imgid)
return info
def get_heif_exif(imgfile):
# 读取HEIC文件
heif_file = pyheif.read(imgfile)
# 将HEIC转换为PIL图像对象
image = Image.frombytes(
heif_file.mode,
heif_file.size,
heif_file.data,
"raw",
heif_file.mode,
heif_file.stride,
)
# 提取EXIF数据
exif_data = image.getexif()
# 将EXIF数据转换为可读格式
exif_data_dict = {
ExifTags.TAGS.get(k, k): v
for k, v in exif_data.items()
if k in ExifTags.TAGS
}
return exif_data_dict
def convert_to_degrees(value):
d = float(value.values[0].num) / float(value.values[0].den)
m = float(value.values[1].num) / float(value.values[1].den)
s = float(value.values[2].num) / float(value.values[2].den)
return d + (m / 60.0) + (s / 3600.0)
def get_image_info(imgfile):
"""
从EXIF数据中提取纬度和经度信息
"""
exif_data = None
if imgfile.lower().endswith('heic'):
exif_data = get_heif_exif(imgfile)
else:
with open(imgfile, 'rb') as f:
exif_data = exifread.process_file(f)
if exif_data is None:
return None, None, None
# 经度
timestamp = None
if 'EXIF DateTimeOriginal' in exif_data:
timestamp = str(exif_data['EXIF DateTimeOriginal'])
timestamp = timestamp[:19]
lon_ref = exif_data.get('GPS GPSLongitudeRef')
lon = exif_data.get('GPS GPSLongitude')
lat_ref = exif_data.get('GPS GPSLatitudeRef')
lat = exif_data.get('GPS GPSLatitude')
glon = None
glat = None
try:
if lon_ref and lon:
glon = convert_to_degrees(lon)
if lon_ref.values == 'W':
glon = -lon
# 纬度
if lat_ref and lat:
glat = convert_to_degrees(lat)
if lat_ref.values == 'S':
glat = -lat
except:
pass
print(f'{imgfile=}, {glat=}, {glon=}, {timestamp=}')
return glat, glon, timestamp

215
app/loadmedia.py Normal file
View File

@ -0,0 +1,215 @@
import os, sys
import json
import asyncio
import argparse
import fingerprint
from traceback import format_exc
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.log import debug, exception
from appPublic.worker import awaitify
from appPublic.dictObject import DictObject
from appPublic.timeUtils import curDateString, timestampstr
from appPublic.uniqueID import getID
from appPublic.folderUtils import listFile, _mkdir
from ahserver.filestorage import FileStorage
import av
from PIL import Image
from imageinfo import get_image_info, get_imagefaces
from pillow_heif import register_heif_opener
register_heif_opener()
def get_thumbnail(mloc, loctype, userid):
fs = FileStorage()
if loctype == '0': # webpath
mloc1 = fs.realPath(mloc)
mloc = mloc1
# Open the original image
with Image.open(mloc) as img:
# Define the size of the thumbnail
thumbnail_size = (256, 256) # for example, 128x128 pixels
# Create the thumbnail
img.thumbnail(thumbnail_size)
name = os.path.basename(mloc).split('.')[0] + '.jpg'
fn = fs._name2path(name, userid=userid)
webpath = fs.webpath(fn)
_mkdir(os.path.dirname(fn))
# Save the thumbnail to disk
img.save(fn, 'JPEG')
return webpath
def get_1st_keyframe(mloc, loctype, userid):
# Open the video file
fs = FileStorage()
if loctype == '0': # webpath
mloc1 = fs.realPath(mloc)
mloc = mloc1
container = av.open(mloc)
# Iterate through the video to find the first key frame
kcnt = 0
for frame in container.decode(video=0):
if frame.key_frame:
kcnt += 1
if kcnt > 10:
# Save the key frame as an image
name = os.path.basename(mloc).split('.')[0] + '.jpg'
fn = fs._name2path(name, userid=userid)
webpath = fs.webpath(fn)
_mkdir(os.path.dirname(fn))
frame.to_image().save(fn)
return webpath
async def save_image_info(sor, userid, imgid, path, ownerid):
f = awaitify(get_image_info)
lat, lon, ts = await f(path)
finfo = await awaitify(get_imagefaces)(path, imgid, ownerid)
ns1 = {
"id":imgid
}
if ts is None:
ns1["info_status"] = '0'
else:
ns1["info_status"] = "1"
ns1["latitude"] = lat
ns1["longitude"] = lon
ns1["media_timestamp"] = ts
ns1["face_cnt"] = len(finfo['faces'])
await sor.C('imageinfo', ns1.copy())
for f in finfo['faces']:
ns2 = {
'id': getID(),
'imageid':imgid,
'faceid':f['id']
}
await sor.C('imageface', ns2)
async def save_image(sor, path, webpath, ownerid):
ns = DictObject()
ns.id = getID()
ns.name = os.path.basename(path).rsplit('.', 1)[0]
ns.mtype = 'image'
ns.mlocation = webpath
ns.mloctype = '0'
ns.logo_url = get_thumbnail(ns.mlocation, ns.mloctype, ownerid)
ns.create_date = curDateString()
ns.ownerid = ownerid
ns.del_flg = '0'
refer_cnt = 1
await sor.C('media', ns.copy())
# await save_image_info(sor, ns.id, path, ownerid)
return
async def save_audio(sor, path, webpath, ownerid):
ns = DictObject()
ns.id = getID()
ns.name = os.path.basename(path).rsplit('.', 1)[0]
ns.mtype = 'audio'
ns.mlocation = webpath
ns.mloctype = '0'
ns.create_date = curDateString()
ns.ownerid = ownerid
ns.del_flg = '0'
refer_cnt = 1
await sor.C('media', ns.copy())
async def save_video(sor, path, webpath, ownerid):
ns = DictObject()
ns.id = getID()
ns.name = os.path.basename(path).rsplit('.', 1)[0]
ns.mtype = 'video'
ns.mlocation = webpath
ns.mloctype = '0'
ns.logo_url = get_1st_keyframe(ns.mlocation, ns.mloctype, ownerid)
ns.create_date = curDateString()
ns.ownerid = ownerid
ns.del_flg = '0'
refer_cnt = 1
await sor.C('media', ns.copy())
ftyp_f = {
"jpg":save_image,
"jpeg":save_image,
"heic":save_image,
"gif":save_image,
"png":save_image,
"cr2":save_image,
"mkv":save_video,
"flv":save_video,
"webm":save_video,
"avi":save_video,
"mov":save_video,
"mp4":save_video,
"wmv":save_video,
"mp3":save_audio,
"aiff":save_audio,
"ape":save_audio,
"wav":save_audio,
"flac":save_audio,
"aac":save_audio,
"ogg":save_audio,
"wma":save_audio,
"alac":save_audio,
}
async def save_media(f, webpath, userid):
fs = FileStorage()
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
if f is None:
f = fs.realPath(webpath)
recs = await sor.R('media', {'mlocation':webpath, 'mloctype':'0'})
if len(recs) > 0:
debug(f'{f} already exists')
return
sub = f.split('.')[-1].lower()
func = ftyp_f.get(sub)
if func:
try:
r = await func(sor, f, webpath, userid)
debug(f'{f} loaded ...')
return True
except Exception as e:
x = format_exc()
debug(f'{f} is not loaded{e}\n{x}')
else:
debug(f'{f} is not loaded')
return False
async def load_media(folder, username=None):
folder = os.path.abspath(folder)
debug(f'handle {folder} ...')
fs = FileStorage()
if not folder.startswith(fs.root):
debug(f'{folder=} must start with {fs.root}')
raise Exception(f'{folder=} must start with {fs.root}')
userid = None
if username:
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
users = await sor.R('users', {'username':username})
if len(users) > 0:
userid = users[0].id
for f in listFile(folder, rescursive=True):
webpath = fs.webpath(f)
await save_media(f, webpath, userid)
if __name__ == '__main__':
async def main():
parser = argparse.ArgumentParser(prog=sys.argv[0])
parser.add_argument('-w', '--workdir')
parser.add_argument('-u', '--username')
parser.add_argument('folders', nargs='*')
args = parser.parse_args()
workdir = args.workdir or os.getcwd()
config = getConfig(workdir, {'workdir':workdir})
DBPools(config.databases)
for folder in args.folders:
await load_media(folder, username=args.username or 'ymq')
loop = asyncio.new_event_loop()
loop.run_until_complete(main())

43
app/media.py Normal file
View File

@ -0,0 +1,43 @@
import os
from ahserver.webapp import webapp
from ahserver.filestorage import FileStorage
from ahserver.serverenv import ServerEnv
from appPublic.registerfunction import RegisterFunction
from appPublic.log import debug
from appPublic.worker import awaitify
from appbase.init import load_appbase
from rbac.init import load_rbac
from findperson.init import load_findperson
from ext import init_ui_ext
from loadmedia import save_media, load_media
from imageface import create_knownface
def runytdlp(cookie_webpath, url):
# we have a dl in ~/bin, and ~bin in PATH
fs = FileStorage()
cookiefile = fs.realPath(cookie_webpath)
cmd = f"""cd /d/ymq/media/dl; yt-dlp --cookies {cookiefile} --proxy socks5://127.0.0.1:1086 -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a] /best[ext=mp4]/best' "{url}" """
debug(f'execute:{cmd}')
os.system(cmd)
def get_module_dbname(mn):
return 'mediadb'
def init():
g = ServerEnv()
g.get_module_dbname = get_module_dbname
g.runytdlp = awaitify(runytdlp)
g.create_knownface = create_knownface
g.load_media = load_media
g.save_media = save_media
rf = RegisterFunction()
rf.register('get_module_dbname', get_module_dbname)
init_ui_ext()
load_appbase()
load_rbac()
load_findperson()
if __name__ == '__main__':
webapp(init)

33
app/t.py Normal file
View File

@ -0,0 +1,33 @@
import asyncio
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from ahserver.filestorage import FileStorage
from imageinfo import get_image_info
async def main():
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
sql = "select a.* from media a left join mediainfo b on a.id = b.id where b.id is null and a.mtype = 'image'"
recs = await sor.sqlExe(sql, {})
fs = FileStorage()
for r in recs:
fp = fs.realPath(r.mlocation)
print(fp, '...')
lat, lon, ts = get_image_info(fp)
info_status = '0'
if ts:
info_status = '1'
ns = {
"id":r.id,
"info_status":info_status,
"latitude":lat,
"longitude":lon,
"media_timestamp":ts
}
await sor.C('mediainfo', ns)
if __name__ == '__main__':
config = getConfig('..', {'workdir':'..'})
DBPools(config.databases)
asyncio.new_event_loop().run_until_complete(main())

61
app/videokeyframe.py Normal file
View File

@ -0,0 +1,61 @@
import asyncio
import os, sys
import argparse
import json
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.log import debug
from appPublic.dictObject import DictObject
from appPublic.timeUtils import curDateString, timestampstr
from appPublic.uniqueID import getID
from appPublic.folderUtils import listFile, _mkdir
from ahserver.filestorage import FileStorage
import av
def get_1st_keyframe(mloc, loctype, userid):
# Open the video file
fs = FileStorage()
if loctype == '0': # webpath
mloc1 = fs.realPath(mloc)
debug(f'{mloc=}, {mloc1=}')
mloc = mloc1
container = av.open(mloc)
# Iterate through the video to find the first key frame
kcnt = 0
for frame in container.decode(video=0):
if frame.key_frame:
kcnt += 1
if kcnt > 10:
# Save the key frame as an image
name = os.path.basename(mloc).split('.')[0] + '.jpg'
fn = fs._name2path(name, userid=userid)
webpath = fs.webpath(fn)
_mkdir(os.path.dirname(fn))
frame.to_image().save(fn)
return webpath
async def gen_video_keyframe():
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
sql = "select * from media where mtype = 'video' and logo_url is null and mloctype in ('0', '1')"
recs = await sor.sqlExe(sql, {})
for r in recs:
wp = get_1st_keyframe(r.mlocation, r.mloctype, r.ownerid)
ns = {'id':r.id, 'logo_url':wp}
await sor.U('media', ns)
async def main():
parser = argparse.ArgumentParser(prog=sys.argv[0])
parser.add_argument('-w', '--workdir')
args = parser.parse_args()
workdir = args.workdir or os.getcwd()
config = getConfig(workdir, {'workdir':workdir})
DBPools(config.databases)
await gen_video_keyframe()
if __name__ == '__main__':
loop = asyncio.new_event_loop()
loop.run_until_complete(main())

2
appbase_roleperm.sh Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/bash
python ~/py/rbac/script/roleperm.py mediadb rbac owner superuser appcodes appcodes_kv params

83
conf/config.json Executable file
View File

@ -0,0 +1,83 @@
{
"logger":{
"name":"media",
"levelname":"debug",
"logfile":"$[workdir]$/logs/media.log"
},
"filesroot":"/d/ymq/media",
"vectordb_path":"$[workdir]$/vdb/media.db",
"databases":{
"mediadb":{
"driver":"aiomysql",
"async_mode":true,
"coding":"utf8",
"dbname":"mediadb",
"maxconn":100,
"kwargs":{
"user":"test",
"db":"mediadb",
"password":"QUZVcXg5V1p1STMybG5Ia6mX9D0v7+g=",
"host":"localhost"
}
}
},
"website":{
"paths":[
["$[workdir]$/wwwroot",""]
],
"client_max_size":10000,
"host":"0.0.0.0",
"port":10182,
"coding":"utf-8",
"ssl_gg":{
"crtfile":"$[workdir]$/conf/www.bsppo.com.pem",
"keyfile":"$[workdir]$/conf/www.bsppo.com.key"
},
"indexes":[
"index.html",
"index.tmpl",
"index.ui",
"index.dspy",
"index.md"
],
"processors":[
[".ws","ws"],
[".xterm","xterm"],
[".proxy","proxy"],
[".llm", "llm"],
[".xlsxds","xlsxds"],
[".sqlds","sqlds"],
[".tmpl.js","tmpl"],
[".tmpl.css","tmpl"],
[".html.tmpl","tmpl"],
[".bcrud", "bricks_crud"],
[".tmpl","tmpl"],
[".bui","bui"],
[".ui","bui"],
[".dspy","dspy"],
[".md","md"]
],
"startswiths":[
{
"leading":"/idfile",
"registerfunction":"idfile"
}
],
"rsakey":{
"privatekey":"$[workdir]$/conf/rsa_private_key.pem",
"publickey":"$[workdir]$/conf/rsa_public_key.pem"
},
"session_max_time":3000,
"session_issue_time":2500,
"session_redis":{
"url":"redis://127.0.0.1:6379"
}
},
"langMapping":{
"zh-Hans-CN":"zh-cn",
"zh-CN":"zh-cn",
"en-us":"en",
"en-US":"en"
}
}

24
json/media.json Normal file
View File

@ -0,0 +1,24 @@
{
"tblname":"media",
"aliasoops":"video",
"params":{
"toolbar":{
"tools":[
{
"name":"play",
"label":"play",
"url":"{{entire_url('../ext/play_video.ui')}}"
}
]
},
"sortby":"name",
"browserfields": {
"exclouded": ["id" ],
"cwidth": {}
},
"logined_userid":"ownerid",
"editexclouded": [
"id"
]
}
}

BIN
models/imageface.xlsx Normal file

Binary file not shown.

BIN
models/imageinfo.xlsx Normal file

Binary file not shown.

BIN
models/knownface.xlsx Normal file

Binary file not shown.

BIN
models/media.xlsx Normal file

Binary file not shown.

BIN
models/mediainfo.xlsx Normal file

Binary file not shown.

47
models/mysql.ddl.sql Normal file
View File

@ -0,0 +1,47 @@
drop table if exists imageinfo;
CREATE TABLE imageinfo
(
`id` VARCHAR(32) comment 'id',
`info_status` VARCHAR(1) comment '信息状态',
`latitude` double(16,8) comment '纬度',
`longitude` double(16,8) comment '经度',
`media_timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '时间戳',
`face_cnt` int comment '脸数'
,primary key(id)
)
engine=innodb
default charset=utf8
comment '图片信息'
;
-- ./imageface.xlsx
drop table if exists imageface;
CREATE TABLE imageface
(
`id` VARCHAR(32) comment 'id',
`imageid` VARCHAR(32) comment 'imageid',
`faceid` VARCHAR(32) comment 'faceid'
,primary key(id)
)
engine=innodb
default charset=utf8
comment '图片人脸'
;

7
rbac_roleperm.sh Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/bash
python ~/py/rbac/script/roleperm.py mediadb rbac owner superuser role permission rolepermission user user role organization orgtypes
python ~/py/rbac/script/roleperm.py mediadb rbac owner admin user userrole
python ~/py/rbac/script/roleperm.py mediadb rbac customer admin user userrole
# python ~/py/rbac/script/roleperm.py mediadb rbac reseller admin user userrole

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fingerprint
pillow
pyheif
exifread
pillow_heif

98
script/media.nginx Normal file
View File

@ -0,0 +1,98 @@
server {
autoindex on;
client_max_body_size 20g;
server_name kymoz.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Url "$scheme://$host:$server_port$request_uri";
index index.html index.htm;
location ~^/ip$ {
return 200 "$remote_addr";
}
location ^~ /wss/ {
proxy_pass http://localhost:10183/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prepath "wss";
}
location ^~ /msp\/wss/ {
proxy_pass http://localhost:10101/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prepath "msp/wss";
}
location ^~ /msp/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Origin *;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-real-ip $remote_addr;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
proxy_set_header X-Forwarded-Prepath 'msp';
proxy_pass http://localhost:10100/;
}
location ^~ /tv/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Origin *;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-real-ip $remote_addr;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
proxy_set_header X-Forwarded-Prepath 'tv';
proxy_pass http://localhost:10180/;
}
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Origin *;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-real-ip $remote_addr;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
proxy_pass http://localhost:10182/;
}
listen 10443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/kymoz.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/kymoz.com/privkey.pem; # managed by Certbot
}
server {
if ($host = kymoz.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 10080;
server_name kymoz.com;
return 404; # managed by Certbot
}

16
script/media.service Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=media service
Documention=media service to control media service start or stoop
After=mariadb.service
Wants=systemd-networkd.service
Requires=nginx.service
[Service]
User=ymq
Group=ymq
Type=forking
ExecStart="/home/ymq/py/media/script/run.sh"
ExecStop="/home/ymq/py/media/script/stop.sh"
[Install]
WantedBy=multi-user.target

5
script/run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/bash
$HOME/py3/bin/python $HOME/py/media/app/media.py -w $HOME/py/media >$HOME/py/media/logs/stderr.log 2>&1 &
$HOME/py3/bin/python $HOME/py/media/app/media.py -w $HOME/py/media -p 10183 >>$HOME/py/media/logs/stderr.log 2>&1 &
exit 0

3
script/stop.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bin
/home/ymq/bin/killname app/media.py

42
wwwroot/app_panel.ui Normal file
View File

@ -0,0 +1,42 @@
{
"widgettype":"HBox",
"options":{
"css":"filler"
},
"subwidgets":[
{
"widgettype":"Image",
"options":{
"height":"100%",
"css":"clickable",
"url":"{{entire_url('/imgs/media.png')}}"
},
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"Popup",
"popup_options":{
"cwidth":10,
"eventpos":true,
"dismiss_events":["command"]
},
"options":{
"url":"{{entire_url('menu.ui')}}"
}
}
]
},
{
"widgettype":"Title4",
"options":{
"i18n":true,
"otext":"媒体平台",
"wrap":true,
"halign":"left"
}
}
]
}

1
wwwroot/appbase Symbolic link
View File

@ -0,0 +1 @@
/home/ymq/py/appbase/wwwroot

View File

@ -0,0 +1,46 @@
{
"id":"audiolist",
"widgettype":"Cols",
"options":{
"css":"filler",
"data_url":"{{entire_url('get_audios.dspy')}}",
"data_params":{
},
"mobile_cols":2,
"col_cwidth":16,
"col_cheight":12,
"record_view":{
"widgettype":"VBox",
"options":{
"css":"card"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"text":"${name}"
}
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"width":"80%",
"height":"80%"
},
"options":{
"url":"{{entire_url('audioplay.dspy')}}",
"params":{
"id":"${id}"
}
}
}
]
}
}
}

View File

@ -0,0 +1,21 @@
ns = {
'id':params_kw.id
}
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
recs = await sor.R('media', ns)
if len(recs) >= 1:
r = recs[0]
mtype = r.mlocation.rsplit('.',1)[-1]
typ = f'audio/{mtype}'
return {
"widgettype":"AudioPlayer",
"id":"player",
"options":{
"height":"99%",
"width":"30%",
"url":entire_url('/idfile') + "?path=" + r.mlocation,
"autoplay":True
}
}

View File

@ -0,0 +1,34 @@
ns = {
'page':params_kw.get('page', 1),
'sort':'name',
'rowpages':80
}
userid = await get_user()
if not userid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userid
sql = """select * from media where mtype = 'audio' and (ownerid=${ownerid}$ or ownerid is NULL)"""
if params_kw.search:
b = params_kw.search.split(' ')
b.insert(0, '')
b.append('')
loc = '%'.join(b)
ns['loc'] = loc
sql += " and mlocation like ${loc}$"
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
r = await sor.sqlPaging(sql, ns.copy())
return r
return {
'total':0,
'rows':[]
}

37
wwwroot/audio/index.ui Normal file
View File

@ -0,0 +1,37 @@
{
"widgettype":"VBox",
"options":{
"height":"100%",
"width":"100%"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"i18n":true,
"otext":"音频"
}
},
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('search.ui')}}"
}
},
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('audiolist.ui')}}"
}
}
],
"binds":[
{
"wid":"search",
"event":"changed",
"actiontype":"script",
"target":"audiolist",
"script":"console.log(this, params); this.load_first_page(params)"
}
]
}

32
wwwroot/audio/search.ui Normal file
View File

@ -0,0 +1,32 @@
{
"widgettype":"HBox",
"options":{
"cheight":2,
"width":"100%",
"dynsiz":true
},
"subwidgets":[
{
"widgettype":"Text",
"options":{
"width":"auto",
"otext":"搜索",
"i18n":true
}
},
{
"widgettype":"Filler",
"options":{},
"subwidgets":[
{
"id":"search",
"widgettype":"UiStr",
"options":{
"name":"search",
"width":"100%"
}
}
]
}
]
}

17
wwwroot/bottom.ui Normal file
View File

@ -0,0 +1,17 @@
{
"widgettype":"HBox",
"options":{
"cheight":2,
"bgcolor":"#e5e5e5"
},
"subwidgets":[
{
"widgettype":"Text",
"options":{
"otext":"© 2024 版权所有",
"i18n":true,
"wrap":true
}
}
]
}

1
wwwroot/bricks Symbolic link
View File

@ -0,0 +1 @@
/tmp/dist

15
wwwroot/center.ui Normal file
View File

@ -0,0 +1,15 @@
{
"widgettype":"VBox",
"id":"page_center",
"options":{
"css":"filler"
},
"subwidgets":[
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('public')}}"
}
}
]
}

View File

@ -0,0 +1,4 @@
debug(f'{params_kw=}')
userid = await get_user()
await runytdlp(params_kw.cookiepath, params_kw.url)
await load_media('/d/ymq/media/dl', userid)

33
wwwroot/download_media.ui Normal file
View File

@ -0,0 +1,33 @@
{
"id":"download",
"widgettype":"Form",
"options":{
"title":"从网上下载媒体",
"description":"需要将网站的cookie和响应媒体的url上传过来",
"fields":[
{
"name":"cookiepath",
"label":"网站cookie",
"uitype":"file",
"required":true
},
{
"name":"url",
"label":"媒体页url",
"uitype":"str",
"required":true
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"PopupWindow",
"options":{
"url":"{{entire_url('download_media.dspy')}}"
}
}
]
}

1
wwwroot/enpwd.dspy Normal file
View File

@ -0,0 +1 @@
return password_encode('111111')

6
wwwroot/findperson.dspy Normal file
View File

@ -0,0 +1,6 @@
debug(f'{params_kw=}')
fp = realpath(params_kw.mediafile)
faces = await find_face_in_image(fp, 100)
ids = [f.entity.imgid for f in faces]
debug(f'{faces=}, {ids=}')
await redirect(entire_url('/idsimages') + '?ids=' + ','.join(ids))

37
wwwroot/findperson.ui Normal file
View File

@ -0,0 +1,37 @@
{% if get_user() %}
{
"id":"uplaod",
"widgettype":"Form",
"options":{
"title":"上传照片",
"description":"可以上传人物图片文件",
"submit_url_oops":"{{entire_url('findperson.dspy')}}",
"fields":[
{
"name":"mediafile",
"uitype":"file",
"required":true
}
]
},
"binds":[
{
"wid":"self",
"event":"submit",
"actiontype":"urlwidget",
"target":"PopupWindow",
"options":{
"url":"{{entire_url('findperson.dspy')}}"
}
}
]
}
{% else %}
{
"widgettype":"Message",
"options":{
"title":"Error",
"message":"You need login"
}
}
{% endif %}

View File

@ -0,0 +1,16 @@
ns = {
"ids": params_kw.ids.split(','),
'page':params_kw.get('page', 1),
'sort':'name',
'rowpages':80
}
sql = """select * from media where id in ${ids}$"""
debug(f'{sql=}, {ns=}')
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
r = await sor.sqlPaging(sql, ns.copy())
return r
return {
'total':0,
'rows':[]
}

View File

@ -0,0 +1,61 @@
{
"id":"imagelist",
"widgettype":"Cols",
"options":{
"css":"filler",
"data_url":"{{entire_url('get_images.dspy')}}",
"data_params":{
"ids":"{{params_kw.ids}}"
},
"mobile_cols":2,
"col_cwidth":16,
"col_cheight":12,
"record_view":{
"widgettype":"VBox",
"options":{
"css":"card"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"text":"${name}"
}
},
{
"widgettype":"Filler",
"options":{
},
"subwidgets":[
{
"widgettype":"Image",
"options":{
"width":"100%",
"url":"{{entire_url('/idfile')}}?path=${logo_url}"
}
}
]
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"width":"80%",
"height":"80%"
},
"options":{
"url":"{{entire_url('imageplay.dspy')}}",
"params":{
"id":"${id}"
}
}
}
]
}
}
}

View File

@ -0,0 +1,51 @@
ns = {
'id':params_kw.id
}
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
recs = await sor.R('media', ns)
if len(recs) >= 1:
r = recs[0]
return {
"widgettype":"VBox",
"options":{},
"subwidgets":[
{
"widgettype":"Filler",
"options":{},
"subwidgets":[
{
"widgettype":"Image",
"id":"player",
"options":{
"witdh":"100%",
"height":"100%",
"url":entire_url('/idfile') + "?path=" + r.mlocation,
}
}
]
},
{
"id":"iconbar",
"widgettype":"IconBar",
"options":{
"cheight":2,
"tools":[
{
"name":"download",
"icon":entire_url('/imgs/download.png'),
}
]
}
}
],
"binds":[
{
"wid":"iconbar",
"event":"download",
"actiontype":"script",
"target":"self",
"script":"open('" + entire_url('/idfile') + "?path=" + r.mlocation + "&download=1');"
}]
}

View File

@ -0,0 +1,25 @@
{
"widgettype":"VBox",
"options":{
"height":"100%",
"width":"100%"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"i18n":true,
"otext":"图片"
}
},
{
"widgettype":"urlwidget",
"options":{
"params":{
"ids":"{{params_kw.ids}}"
},
"url":"{{entire_url('imagelist.ui')}}"
}
}
]
}

View File

@ -0,0 +1,32 @@
{
"widgettype":"HBox",
"options":{
"cheight":2,
"width":"100%",
"dynsiz":true
},
"subwidgets":[
{
"widgettype":"Text",
"options":{
"width":"auto",
"otext":"搜索",
"i18n":true
}
},
{
"widgettype":"Filler",
"options":{},
"subwidgets":[
{
"id":"search",
"widgettype":"UiStr",
"options":{
"name":"search",
"width":"100%"
}
}
]
}
]
}

32
wwwroot/idsimages/t.ui Normal file
View File

@ -0,0 +1,32 @@
{
"widgettype": "VBox",
"options": {},
"subwidgets": [
{
"widgettype": "Filler",
"options": {},
"subwidgets": [
{
"widgettype": "Image",
"id": "player",
"options": {
"witdh": "100%",
"height": "100%",
"url": "https://kymoz.com:10443/idfile?path=/images/\u4f59\u9526\u6d53/100\u5929/002.JPG"
}
}
]
}
]
"binds": [
{
"wid": "iconbar",
"event": "download",
"actiontype": "urlwidget",
"target": "self",
"options": {
"url": "https://kymoz.com:10443/download?path=/images/\u4f59\u9526\u6d53/100\u5929/002.JPG"
}
}
]
}

View File

@ -0,0 +1,34 @@
ns = {
'page':params_kw.get('page', 1),
'sort':'name',
'rowpages':80
}
userid = await get_user()
if not userid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userid
sql = """select * from media where mtype = 'image' and (ownerid=${ownerid}$ or ownerid is NULL)"""
if params_kw.search:
b = params_kw.search.split(' ')
b.insert(0, '')
b.append('')
loc = '%'.join(b)
ns['loc'] = loc
sql += " and mlocation like ${loc}$"
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
r = await sor.sqlPaging(sql, ns.copy())
return r
return {
'total':0,
'rows':[]
}

View File

@ -0,0 +1,60 @@
{
"id":"imagelist",
"widgettype":"Cols",
"options":{
"css":"filler",
"data_url":"{{entire_url('get_images.dspy')}}",
"data_params":{
},
"mobile_cols":2,
"col_cwidth":16,
"col_cheight":12,
"record_view":{
"widgettype":"VBox",
"options":{
"css":"card"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"text":"${name}"
}
},
{
"widgettype":"Filler",
"options":{
},
"subwidgets":[
{
"widgettype":"Image",
"options":{
"width":"100%",
"url":"{{entire_url('/idfile')}}?path=${logo_url}"
}
}
]
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"width":"80%",
"height":"80%"
},
"options":{
"url":"{{entire_url('imageplay.dspy')}}",
"params":{
"id":"${id}"
}
}
}
]
}
}
}

View File

@ -0,0 +1,51 @@
ns = {
'id':params_kw.id
}
db = DBPools()
async with db.sqlorContext('mediadb') as sor:
recs = await sor.R('media', ns)
if len(recs) >= 1:
r = recs[0]
return {
"widgettype":"VBox",
"options":{},
"subwidgets":[
{
"widgettype":"Filler",
"options":{},
"subwidgets":[
{
"widgettype":"Image",
"id":"player",
"options":{
"witdh":"100%",
"height":"100%",
"url":entire_url('/idfile') + "?path=" + r.mlocation,
}
}
]
},
{
"id":"iconbar",
"widgettype":"IconBar",
"options":{
"cheight":2,
"tools":[
{
"name":"download",
"icon":entire_url('/imgs/download.png'),
}
]
}
}
],
"binds":[
{
"wid":"iconbar",
"event":"download",
"actiontype":"script",
"target":"self",
"script":"open('" + entire_url('/idfile') + "?path=" + r.mlocation + "&download=1');"
}]
}

37
wwwroot/image/index.ui Normal file
View File

@ -0,0 +1,37 @@
{
"widgettype":"VBox",
"options":{
"height":"100%",
"width":"100%"
},
"subwidgets":[
{
"widgettype":"Title4",
"options":{
"i18n":true,
"otext":"图片"
}
},
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('search.ui')}}"
}
},
{
"widgettype":"urlwidget",
"options":{
"url":"{{entire_url('imagelist.ui')}}"
}
}
],
"binds":[
{
"wid":"search",
"event":"changed",
"actiontype":"script",
"target":"imagelist",
"script":"console.log(this, params); this.load_first_page(params)"
}
]
}

32
wwwroot/image/search.ui Normal file
View File

@ -0,0 +1,32 @@
{
"widgettype":"HBox",
"options":{
"cheight":2,
"width":"100%",
"dynsiz":true
},
"subwidgets":[
{
"widgettype":"Text",
"options":{
"width":"auto",
"otext":"搜索",
"i18n":true
}
},
{
"widgettype":"Filler",
"options":{},
"subwidgets":[
{
"id":"search",
"widgettype":"UiStr",
"options":{
"name":"search",
"width":"100%"
}
}
]
}
]
}

32
wwwroot/image/t.ui Normal file
View File

@ -0,0 +1,32 @@
{
"widgettype": "VBox",
"options": {},
"subwidgets": [
{
"widgettype": "Filler",
"options": {},
"subwidgets": [
{
"widgettype": "Image",
"id": "player",
"options": {
"witdh": "100%",
"height": "100%",
"url": "https://kymoz.com:10443/idfile?path=/images/\u4f59\u9526\u6d53/100\u5929/002.JPG"
}
}
]
}
]
"binds": [
{
"wid": "iconbar",
"event": "download",
"actiontype": "urlwidget",
"target": "self",
"options": {
"url": "https://kymoz.com:10443/download?path=/images/\u4f59\u9526\u6d53/100\u5929/002.JPG"
}
}
]
}

BIN
wwwroot/imgs/baichuan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
wwwroot/imgs/bc_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/bc_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
wwwroot/imgs/bc_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/bl_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/bl_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
wwwroot/imgs/bl_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/br_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/br_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
wwwroot/imgs/br_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/chatgpt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
wwwroot/imgs/cl_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/cl_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
wwwroot/imgs/cl_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/cr_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/cr_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
wwwroot/imgs/cr_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/deepseek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
wwwroot/imgs/dot_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
wwwroot/imgs/doubao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
wwwroot/imgs/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
wwwroot/imgs/ii_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/ii_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
wwwroot/imgs/ii_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/imgs/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
wwwroot/imgs/like.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/imgs/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
wwwroot/imgs/logout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
wwwroot/imgs/media.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
wwwroot/imgs/minimax.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
wwwroot/imgs/moonshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
wwwroot/imgs/ollama.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
wwwroot/imgs/people.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/imgs/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
wwwroot/imgs/qianfan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
wwwroot/imgs/qianwen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
wwwroot/imgs/register.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
wwwroot/imgs/sage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
wwwroot/imgs/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
wwwroot/imgs/sensetime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/imgs/submit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/imgs/tc_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/tc_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
wwwroot/imgs/tc_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/tl_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
wwwroot/imgs/tl_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
wwwroot/imgs/tl_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Some files were not shown because too many files have changed in this diff Show More