commit e838d19d9f12dd3552da401da635f65e095c2dad Author: yumoqing Date: Tue Jun 24 11:45:33 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..12abd8d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# media \ No newline at end of file diff --git a/app/__pycache__/ext.cpython-310.pyc b/app/__pycache__/ext.cpython-310.pyc new file mode 100644 index 0000000..b86f0fd Binary files /dev/null and b/app/__pycache__/ext.cpython-310.pyc differ diff --git a/app/__pycache__/loadmedia.cpython-310.pyc b/app/__pycache__/loadmedia.cpython-310.pyc new file mode 100644 index 0000000..d347fe9 Binary files /dev/null and b/app/__pycache__/loadmedia.cpython-310.pyc differ diff --git a/app/add_imageinfo.py b/app/add_imageinfo.py new file mode 100644 index 0000000..f1d2c26 --- /dev/null +++ b/app/add_imageinfo.py @@ -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()) diff --git a/app/ext.py b/app/ext.py new file mode 100644 index 0000000..15d7853 --- /dev/null +++ b/app/ext.py @@ -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 + diff --git a/app/imageface.py b/app/imageface.py new file mode 100644 index 0000000..2b84f39 --- /dev/null +++ b/app/imageface.py @@ -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) + diff --git a/app/imageinfo.py b/app/imageinfo.py new file mode 100644 index 0000000..5b8297c --- /dev/null +++ b/app/imageinfo.py @@ -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 + diff --git a/app/loadmedia.py b/app/loadmedia.py new file mode 100644 index 0000000..9c070d9 --- /dev/null +++ b/app/loadmedia.py @@ -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()) + diff --git a/app/media.py b/app/media.py new file mode 100644 index 0000000..29ee8c2 --- /dev/null +++ b/app/media.py @@ -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) + diff --git a/app/t.py b/app/t.py new file mode 100644 index 0000000..5cb7962 --- /dev/null +++ b/app/t.py @@ -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()) + diff --git a/app/videokeyframe.py b/app/videokeyframe.py new file mode 100644 index 0000000..b9016ff --- /dev/null +++ b/app/videokeyframe.py @@ -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()) + diff --git a/appbase_roleperm.sh b/appbase_roleperm.sh new file mode 100755 index 0000000..46b5f4a --- /dev/null +++ b/appbase_roleperm.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +python ~/py/rbac/script/roleperm.py mediadb rbac owner superuser appcodes appcodes_kv params diff --git a/conf/config.json b/conf/config.json new file mode 100755 index 0000000..761ece6 --- /dev/null +++ b/conf/config.json @@ -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" + } +} diff --git a/json/media.json b/json/media.json new file mode 100644 index 0000000..960c4e7 --- /dev/null +++ b/json/media.json @@ -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" + ] + } +} diff --git a/models/imageface.xlsx b/models/imageface.xlsx new file mode 100644 index 0000000..781c450 Binary files /dev/null and b/models/imageface.xlsx differ diff --git a/models/imageinfo.xlsx b/models/imageinfo.xlsx new file mode 100644 index 0000000..3bd2962 Binary files /dev/null and b/models/imageinfo.xlsx differ diff --git a/models/knownface.xlsx b/models/knownface.xlsx new file mode 100644 index 0000000..047eef0 Binary files /dev/null and b/models/knownface.xlsx differ diff --git a/models/media.xlsx b/models/media.xlsx new file mode 100644 index 0000000..de3344f Binary files /dev/null and b/models/media.xlsx differ diff --git a/models/mediainfo.xlsx b/models/mediainfo.xlsx new file mode 100644 index 0000000..97458fb Binary files /dev/null and b/models/mediainfo.xlsx differ diff --git a/models/mysql.ddl.sql b/models/mysql.ddl.sql new file mode 100644 index 0000000..f0ec92e --- /dev/null +++ b/models/mysql.ddl.sql @@ -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 '图片人脸' +; + + diff --git a/rbac_roleperm.sh b/rbac_roleperm.sh new file mode 100644 index 0000000..700ec90 --- /dev/null +++ b/rbac_roleperm.sh @@ -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 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b3e57cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fingerprint +pillow +pyheif +exifread +pillow_heif diff --git a/script/media.nginx b/script/media.nginx new file mode 100644 index 0000000..cc7539e --- /dev/null +++ b/script/media.nginx @@ -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 + + +} diff --git a/script/media.service b/script/media.service new file mode 100644 index 0000000..5ff74ec --- /dev/null +++ b/script/media.service @@ -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 + diff --git a/script/run.sh b/script/run.sh new file mode 100755 index 0000000..4c38841 --- /dev/null +++ b/script/run.sh @@ -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 diff --git a/script/stop.sh b/script/stop.sh new file mode 100755 index 0000000..133c754 --- /dev/null +++ b/script/stop.sh @@ -0,0 +1,3 @@ +#!/bin/bin + +/home/ymq/bin/killname app/media.py diff --git a/wwwroot/app_panel.ui b/wwwroot/app_panel.ui new file mode 100644 index 0000000..00ddc7f --- /dev/null +++ b/wwwroot/app_panel.ui @@ -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" + } + } + ] +} + diff --git a/wwwroot/appbase b/wwwroot/appbase new file mode 120000 index 0000000..e3a6bfb --- /dev/null +++ b/wwwroot/appbase @@ -0,0 +1 @@ +/home/ymq/py/appbase/wwwroot \ No newline at end of file diff --git a/wwwroot/audio/audiolist.ui b/wwwroot/audio/audiolist.ui new file mode 100644 index 0000000..27936f3 --- /dev/null +++ b/wwwroot/audio/audiolist.ui @@ -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}" + } + } + } + ] + } + } +} + diff --git a/wwwroot/audio/audioplay.dspy b/wwwroot/audio/audioplay.dspy new file mode 100644 index 0000000..97dc7dc --- /dev/null +++ b/wwwroot/audio/audioplay.dspy @@ -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 + } + } diff --git a/wwwroot/audio/get_audios.dspy b/wwwroot/audio/get_audios.dspy new file mode 100644 index 0000000..c588ad3 --- /dev/null +++ b/wwwroot/audio/get_audios.dspy @@ -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':[] +} diff --git a/wwwroot/audio/index.ui b/wwwroot/audio/index.ui new file mode 100644 index 0000000..9db237c --- /dev/null +++ b/wwwroot/audio/index.ui @@ -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)" + } + ] +} diff --git a/wwwroot/audio/search.ui b/wwwroot/audio/search.ui new file mode 100644 index 0000000..b54aa1e --- /dev/null +++ b/wwwroot/audio/search.ui @@ -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%" + } + } + ] + } + ] +} diff --git a/wwwroot/bottom.ui b/wwwroot/bottom.ui new file mode 100644 index 0000000..cebb4e8 --- /dev/null +++ b/wwwroot/bottom.ui @@ -0,0 +1,17 @@ +{ + "widgettype":"HBox", + "options":{ + "cheight":2, + "bgcolor":"#e5e5e5" + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"© 2024 版权所有", + "i18n":true, + "wrap":true + } + } + ] +} diff --git a/wwwroot/bricks b/wwwroot/bricks new file mode 120000 index 0000000..8bcfc42 --- /dev/null +++ b/wwwroot/bricks @@ -0,0 +1 @@ +/tmp/dist \ No newline at end of file diff --git a/wwwroot/center.ui b/wwwroot/center.ui new file mode 100644 index 0000000..fbc2e37 --- /dev/null +++ b/wwwroot/center.ui @@ -0,0 +1,15 @@ +{ + "widgettype":"VBox", + "id":"page_center", + "options":{ + "css":"filler" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('public')}}" + } + } + ] +} diff --git a/wwwroot/download_media.dspy b/wwwroot/download_media.dspy new file mode 100644 index 0000000..2b1955b --- /dev/null +++ b/wwwroot/download_media.dspy @@ -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) diff --git a/wwwroot/download_media.ui b/wwwroot/download_media.ui new file mode 100644 index 0000000..e632556 --- /dev/null +++ b/wwwroot/download_media.ui @@ -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')}}" + } + } + ] +} diff --git a/wwwroot/enpwd.dspy b/wwwroot/enpwd.dspy new file mode 100644 index 0000000..366c8e1 --- /dev/null +++ b/wwwroot/enpwd.dspy @@ -0,0 +1 @@ +return password_encode('111111') diff --git a/wwwroot/findperson.dspy b/wwwroot/findperson.dspy new file mode 100644 index 0000000..80d1b13 --- /dev/null +++ b/wwwroot/findperson.dspy @@ -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)) diff --git a/wwwroot/findperson.ui b/wwwroot/findperson.ui new file mode 100644 index 0000000..019e222 --- /dev/null +++ b/wwwroot/findperson.ui @@ -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 %} diff --git a/wwwroot/idsimages/get_images.dspy b/wwwroot/idsimages/get_images.dspy new file mode 100644 index 0000000..0100e90 --- /dev/null +++ b/wwwroot/idsimages/get_images.dspy @@ -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':[] +} diff --git a/wwwroot/idsimages/imagelist.ui b/wwwroot/idsimages/imagelist.ui new file mode 100644 index 0000000..1247387 --- /dev/null +++ b/wwwroot/idsimages/imagelist.ui @@ -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}" + } + } + } + ] + } + } +} + diff --git a/wwwroot/idsimages/imageplay.dspy b/wwwroot/idsimages/imageplay.dspy new file mode 100644 index 0000000..e379cc7 --- /dev/null +++ b/wwwroot/idsimages/imageplay.dspy @@ -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');" + }] +} diff --git a/wwwroot/idsimages/index.ui b/wwwroot/idsimages/index.ui new file mode 100644 index 0000000..cb27779 --- /dev/null +++ b/wwwroot/idsimages/index.ui @@ -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')}}" + } + } + ] +} diff --git a/wwwroot/idsimages/search.ui b/wwwroot/idsimages/search.ui new file mode 100644 index 0000000..b54aa1e --- /dev/null +++ b/wwwroot/idsimages/search.ui @@ -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%" + } + } + ] + } + ] +} diff --git a/wwwroot/idsimages/t.ui b/wwwroot/idsimages/t.ui new file mode 100644 index 0000000..19f7a05 --- /dev/null +++ b/wwwroot/idsimages/t.ui @@ -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" + } + } + ] +} diff --git a/wwwroot/image/get_images.dspy b/wwwroot/image/get_images.dspy new file mode 100644 index 0000000..b314430 --- /dev/null +++ b/wwwroot/image/get_images.dspy @@ -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':[] +} diff --git a/wwwroot/image/imagelist.ui b/wwwroot/image/imagelist.ui new file mode 100644 index 0000000..aa892d5 --- /dev/null +++ b/wwwroot/image/imagelist.ui @@ -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}" + } + } + } + ] + } + } +} + diff --git a/wwwroot/image/imageplay.dspy b/wwwroot/image/imageplay.dspy new file mode 100644 index 0000000..e379cc7 --- /dev/null +++ b/wwwroot/image/imageplay.dspy @@ -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');" + }] +} diff --git a/wwwroot/image/index.ui b/wwwroot/image/index.ui new file mode 100644 index 0000000..3d1c439 --- /dev/null +++ b/wwwroot/image/index.ui @@ -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)" + } + ] +} diff --git a/wwwroot/image/search.ui b/wwwroot/image/search.ui new file mode 100644 index 0000000..b54aa1e --- /dev/null +++ b/wwwroot/image/search.ui @@ -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%" + } + } + ] + } + ] +} diff --git a/wwwroot/image/t.ui b/wwwroot/image/t.ui new file mode 100644 index 0000000..19f7a05 --- /dev/null +++ b/wwwroot/image/t.ui @@ -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" + } + } + ] +} diff --git a/wwwroot/imgs/baichuan.png b/wwwroot/imgs/baichuan.png new file mode 100644 index 0000000..7f8e3f0 Binary files /dev/null and b/wwwroot/imgs/baichuan.png differ diff --git a/wwwroot/imgs/bc_black.png b/wwwroot/imgs/bc_black.png new file mode 100644 index 0000000..7f396b3 Binary files /dev/null and b/wwwroot/imgs/bc_black.png differ diff --git a/wwwroot/imgs/bc_empty.png b/wwwroot/imgs/bc_empty.png new file mode 100644 index 0000000..fe4d97a Binary files /dev/null and b/wwwroot/imgs/bc_empty.png differ diff --git a/wwwroot/imgs/bc_white.png b/wwwroot/imgs/bc_white.png new file mode 100644 index 0000000..2c011f8 Binary files /dev/null and b/wwwroot/imgs/bc_white.png differ diff --git a/wwwroot/imgs/bl_black.png b/wwwroot/imgs/bl_black.png new file mode 100644 index 0000000..0331556 Binary files /dev/null and b/wwwroot/imgs/bl_black.png differ diff --git a/wwwroot/imgs/bl_empty.png b/wwwroot/imgs/bl_empty.png new file mode 100644 index 0000000..8bf28e0 Binary files /dev/null and b/wwwroot/imgs/bl_empty.png differ diff --git a/wwwroot/imgs/bl_white.png b/wwwroot/imgs/bl_white.png new file mode 100644 index 0000000..39b86ee Binary files /dev/null and b/wwwroot/imgs/bl_white.png differ diff --git a/wwwroot/imgs/br_black.png b/wwwroot/imgs/br_black.png new file mode 100644 index 0000000..1e2cf37 Binary files /dev/null and b/wwwroot/imgs/br_black.png differ diff --git a/wwwroot/imgs/br_empty.png b/wwwroot/imgs/br_empty.png new file mode 100644 index 0000000..afb65cb Binary files /dev/null and b/wwwroot/imgs/br_empty.png differ diff --git a/wwwroot/imgs/br_white.png b/wwwroot/imgs/br_white.png new file mode 100644 index 0000000..ba35c19 Binary files /dev/null and b/wwwroot/imgs/br_white.png differ diff --git a/wwwroot/imgs/chatgpt.png b/wwwroot/imgs/chatgpt.png new file mode 100644 index 0000000..1a7dd84 Binary files /dev/null and b/wwwroot/imgs/chatgpt.png differ diff --git a/wwwroot/imgs/cl_black.png b/wwwroot/imgs/cl_black.png new file mode 100644 index 0000000..1afed96 Binary files /dev/null and b/wwwroot/imgs/cl_black.png differ diff --git a/wwwroot/imgs/cl_empty.png b/wwwroot/imgs/cl_empty.png new file mode 100644 index 0000000..efd4f02 Binary files /dev/null and b/wwwroot/imgs/cl_empty.png differ diff --git a/wwwroot/imgs/cl_white.png b/wwwroot/imgs/cl_white.png new file mode 100644 index 0000000..bc533f3 Binary files /dev/null and b/wwwroot/imgs/cl_white.png differ diff --git a/wwwroot/imgs/cr_black.png b/wwwroot/imgs/cr_black.png new file mode 100644 index 0000000..33d4737 Binary files /dev/null and b/wwwroot/imgs/cr_black.png differ diff --git a/wwwroot/imgs/cr_empty.png b/wwwroot/imgs/cr_empty.png new file mode 100644 index 0000000..2b9ce1b Binary files /dev/null and b/wwwroot/imgs/cr_empty.png differ diff --git a/wwwroot/imgs/cr_white.png b/wwwroot/imgs/cr_white.png new file mode 100644 index 0000000..ca99405 Binary files /dev/null and b/wwwroot/imgs/cr_white.png differ diff --git a/wwwroot/imgs/deepseek.png b/wwwroot/imgs/deepseek.png new file mode 100644 index 0000000..9c98e40 Binary files /dev/null and b/wwwroot/imgs/deepseek.png differ diff --git a/wwwroot/imgs/dot_empty.png b/wwwroot/imgs/dot_empty.png new file mode 100644 index 0000000..41257a3 Binary files /dev/null and b/wwwroot/imgs/dot_empty.png differ diff --git a/wwwroot/imgs/doubao.png b/wwwroot/imgs/doubao.png new file mode 100644 index 0000000..dc514e1 Binary files /dev/null and b/wwwroot/imgs/doubao.png differ diff --git a/wwwroot/imgs/download.png b/wwwroot/imgs/download.png new file mode 100644 index 0000000..d3042d8 Binary files /dev/null and b/wwwroot/imgs/download.png differ diff --git a/wwwroot/imgs/ii_black.png b/wwwroot/imgs/ii_black.png new file mode 100644 index 0000000..99fc9fa Binary files /dev/null and b/wwwroot/imgs/ii_black.png differ diff --git a/wwwroot/imgs/ii_empty.png b/wwwroot/imgs/ii_empty.png new file mode 100644 index 0000000..0ae6207 Binary files /dev/null and b/wwwroot/imgs/ii_empty.png differ diff --git a/wwwroot/imgs/ii_white.png b/wwwroot/imgs/ii_white.png new file mode 100644 index 0000000..99fc9fa Binary files /dev/null and b/wwwroot/imgs/ii_white.png differ diff --git a/wwwroot/imgs/key.png b/wwwroot/imgs/key.png new file mode 100644 index 0000000..ceb598a Binary files /dev/null and b/wwwroot/imgs/key.png differ diff --git a/wwwroot/imgs/like.png b/wwwroot/imgs/like.png new file mode 100644 index 0000000..6441c9d Binary files /dev/null and b/wwwroot/imgs/like.png differ diff --git a/wwwroot/imgs/login.png b/wwwroot/imgs/login.png new file mode 100644 index 0000000..eb40e6a Binary files /dev/null and b/wwwroot/imgs/login.png differ diff --git a/wwwroot/imgs/logout.png b/wwwroot/imgs/logout.png new file mode 100644 index 0000000..eafcf76 Binary files /dev/null and b/wwwroot/imgs/logout.png differ diff --git a/wwwroot/imgs/media.png b/wwwroot/imgs/media.png new file mode 100644 index 0000000..93035d8 Binary files /dev/null and b/wwwroot/imgs/media.png differ diff --git a/wwwroot/imgs/minimax.png b/wwwroot/imgs/minimax.png new file mode 100644 index 0000000..142514a Binary files /dev/null and b/wwwroot/imgs/minimax.png differ diff --git a/wwwroot/imgs/moonshot.png b/wwwroot/imgs/moonshot.png new file mode 100644 index 0000000..6f9ae5a Binary files /dev/null and b/wwwroot/imgs/moonshot.png differ diff --git a/wwwroot/imgs/ollama.png b/wwwroot/imgs/ollama.png new file mode 100644 index 0000000..38182d2 Binary files /dev/null and b/wwwroot/imgs/ollama.png differ diff --git a/wwwroot/imgs/people.png b/wwwroot/imgs/people.png new file mode 100644 index 0000000..6a3bc34 Binary files /dev/null and b/wwwroot/imgs/people.png differ diff --git a/wwwroot/imgs/play.png b/wwwroot/imgs/play.png new file mode 100644 index 0000000..dd0d089 Binary files /dev/null and b/wwwroot/imgs/play.png differ diff --git a/wwwroot/imgs/qianfan.png b/wwwroot/imgs/qianfan.png new file mode 100644 index 0000000..013abc9 Binary files /dev/null and b/wwwroot/imgs/qianfan.png differ diff --git a/wwwroot/imgs/qianwen.png b/wwwroot/imgs/qianwen.png new file mode 100644 index 0000000..3d8760e Binary files /dev/null and b/wwwroot/imgs/qianwen.png differ diff --git a/wwwroot/imgs/register.png b/wwwroot/imgs/register.png new file mode 100644 index 0000000..64636cc Binary files /dev/null and b/wwwroot/imgs/register.png differ diff --git a/wwwroot/imgs/sage.png b/wwwroot/imgs/sage.png new file mode 100644 index 0000000..e6a4bd3 Binary files /dev/null and b/wwwroot/imgs/sage.png differ diff --git a/wwwroot/imgs/search.png b/wwwroot/imgs/search.png new file mode 100644 index 0000000..da92fb3 Binary files /dev/null and b/wwwroot/imgs/search.png differ diff --git a/wwwroot/imgs/sensetime.png b/wwwroot/imgs/sensetime.png new file mode 100644 index 0000000..8d82e6f Binary files /dev/null and b/wwwroot/imgs/sensetime.png differ diff --git a/wwwroot/imgs/submit.png b/wwwroot/imgs/submit.png new file mode 100644 index 0000000..bda9bdc Binary files /dev/null and b/wwwroot/imgs/submit.png differ diff --git a/wwwroot/imgs/tc_black.png b/wwwroot/imgs/tc_black.png new file mode 100644 index 0000000..5aca0bb Binary files /dev/null and b/wwwroot/imgs/tc_black.png differ diff --git a/wwwroot/imgs/tc_empty.png b/wwwroot/imgs/tc_empty.png new file mode 100644 index 0000000..ddba8e7 Binary files /dev/null and b/wwwroot/imgs/tc_empty.png differ diff --git a/wwwroot/imgs/tc_white.png b/wwwroot/imgs/tc_white.png new file mode 100644 index 0000000..f931ab2 Binary files /dev/null and b/wwwroot/imgs/tc_white.png differ diff --git a/wwwroot/imgs/tl_black.png b/wwwroot/imgs/tl_black.png new file mode 100644 index 0000000..6fa0768 Binary files /dev/null and b/wwwroot/imgs/tl_black.png differ diff --git a/wwwroot/imgs/tl_empty.png b/wwwroot/imgs/tl_empty.png new file mode 100644 index 0000000..605d91f Binary files /dev/null and b/wwwroot/imgs/tl_empty.png differ diff --git a/wwwroot/imgs/tl_white.png b/wwwroot/imgs/tl_white.png new file mode 100644 index 0000000..07be7eb Binary files /dev/null and b/wwwroot/imgs/tl_white.png differ diff --git a/wwwroot/imgs/tr_black.png b/wwwroot/imgs/tr_black.png new file mode 100644 index 0000000..a960be8 Binary files /dev/null and b/wwwroot/imgs/tr_black.png differ diff --git a/wwwroot/imgs/tr_empty.png b/wwwroot/imgs/tr_empty.png new file mode 100644 index 0000000..6e10ea7 Binary files /dev/null and b/wwwroot/imgs/tr_empty.png differ diff --git a/wwwroot/imgs/tr_white.png b/wwwroot/imgs/tr_white.png new file mode 100644 index 0000000..b5c3cb0 Binary files /dev/null and b/wwwroot/imgs/tr_white.png differ diff --git a/wwwroot/imgs/unlike.png b/wwwroot/imgs/unlike.png new file mode 100644 index 0000000..334f825 Binary files /dev/null and b/wwwroot/imgs/unlike.png differ diff --git a/wwwroot/imgs/upload.png b/wwwroot/imgs/upload.png new file mode 100644 index 0000000..4da3056 Binary files /dev/null and b/wwwroot/imgs/upload.png differ diff --git a/wwwroot/imgs/user.png b/wwwroot/imgs/user.png new file mode 100644 index 0000000..7ef4b96 Binary files /dev/null and b/wwwroot/imgs/user.png differ diff --git a/wwwroot/imgs/weixinzhifu.png b/wwwroot/imgs/weixinzhifu.png new file mode 100644 index 0000000..7bb0735 Binary files /dev/null and b/wwwroot/imgs/weixinzhifu.png differ diff --git a/wwwroot/imgs/zhifubao.png b/wwwroot/imgs/zhifubao.png new file mode 100644 index 0000000..8bd1ad1 Binary files /dev/null and b/wwwroot/imgs/zhifubao.png differ diff --git a/wwwroot/imgs/zhipu.png b/wwwroot/imgs/zhipu.png new file mode 100644 index 0000000..5b5d7cf Binary files /dev/null and b/wwwroot/imgs/zhipu.png differ diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..2cbe938 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,27 @@ +{ + "widgettype":"VBox", + "options":{ + "width":"100%", + "height":"100%" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('top.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('center.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('bottom.ui')}}" + } + } + ] +} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui new file mode 100644 index 0000000..302e186 --- /dev/null +++ b/wwwroot/menu.ui @@ -0,0 +1,51 @@ +{ + "widgettype":"Menu", + "options":{ + "target":"page_center", + "cwidth":10, + "items":[ + { + "name":"home", + "label":"主页", + "url":"{{entire_url('public')}}" + }, + { + "name":"media", + "label":"媒体", + "items":[ + { + "name":"audio", + "label":"声频", + "url":"{{entire_url('audio')}}" + }, + { + "name":"video", + "label":"视频", + "url":"{{entire_url('video')}}" + }, + { + "name":"image", + "label":"图片", + "url":"{{entire_url('image')}}" + }, + { + "name":"download", + "label":"下载", + "url":"{{entire_url('download_media.ui')}}" + }, + { + "name":"findperson", + "label":"找人", + "url":"{{entire_url('findperson.ui')}}" + }, + { + "name":"upload", + "label":"上传", + "url":"{{entire_url('upload_media.ui')}}" + } + ] + } + ] + } +} + diff --git a/wwwroot/public/index.ui b/wwwroot/public/index.ui new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/wwwroot/public/index.ui @@ -0,0 +1,2 @@ +{ +} diff --git a/wwwroot/rbac b/wwwroot/rbac new file mode 120000 index 0000000..04f9b8a --- /dev/null +++ b/wwwroot/rbac @@ -0,0 +1 @@ +/home/ymq/py/rbac/wwwroot \ No newline at end of file diff --git a/wwwroot/top.ui b/wwwroot/top.ui new file mode 100644 index 0000000..2684873 --- /dev/null +++ b/wwwroot/top.ui @@ -0,0 +1,23 @@ +{ + "id":"top_panel", + "widgettype":"HBox", + "options":{ + "bgcolor":"#444444", + "cheight":2.5, + "color":"#eeeeee" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('app_panel.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('/rbac/user/user_panel.ui')}}" + } + } + ] +} diff --git a/wwwroot/upload_media.dspy b/wwwroot/upload_media.dspy new file mode 100644 index 0000000..2ea5a93 --- /dev/null +++ b/wwwroot/upload_media.dspy @@ -0,0 +1,9 @@ +debug(f'{params_kw=}') +userid = await get_user() +if userid is None: + return UiError(title="Error", message="Need login first") + +webpath = params_kw.mediafile +x = await save_media(None, webpath, userid) +return UiMessage(title="Success", message="upload file success") + diff --git a/wwwroot/upload_media.ui b/wwwroot/upload_media.ui new file mode 100644 index 0000000..9c6aafe --- /dev/null +++ b/wwwroot/upload_media.ui @@ -0,0 +1,37 @@ +{% if get_user() %} +{ + "id":"uplaod", + "widgettype":"Form", + "options":{ + "title":"上传媒体", + "description":"可以上传视频,音频和图片文件", + "submit_url_oops":"{{entire_url('upload_media.dspy')}}", + "fields":[ + { + "name":"mediafile", + "uitype":"file", + "required":true + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"submit", + "actiontype":"urlwidget", + "target":"PopupWindow", + "options":{ + "url":"{{entire_url('upload_media.dspy')}}" + } + } + ] +} +{% else %} +{ + "widgettype":"Message", + "options":{ + "title":"Error", + "message":"You need login" + } +} +{% endif %} diff --git a/wwwroot/video/get_videos.dspy b/wwwroot/video/get_videos.dspy new file mode 100644 index 0000000..6106528 --- /dev/null +++ b/wwwroot/video/get_videos.dspy @@ -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 = 'video' 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':[] +} diff --git a/wwwroot/video/index.ui b/wwwroot/video/index.ui new file mode 100644 index 0000000..9bc7a09 --- /dev/null +++ b/wwwroot/video/index.ui @@ -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('videolist.ui')}}" + } + } + ], + "binds":[ + { + "wid":"search", + "event":"changed", + "actiontype":"script", + "target":"videolist", + "script":"console.log(this, params); this.load_first_page(params)" + } + ] +} diff --git a/wwwroot/video/search.ui b/wwwroot/video/search.ui new file mode 100644 index 0000000..b54aa1e --- /dev/null +++ b/wwwroot/video/search.ui @@ -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%" + } + } + ] + } + ] +} diff --git a/wwwroot/video/videolist.ui b/wwwroot/video/videolist.ui new file mode 100644 index 0000000..1dab586 --- /dev/null +++ b/wwwroot/video/videolist.ui @@ -0,0 +1,60 @@ +{ + "id":"videolist", + "widgettype":"Cols", + "options":{ + "css":"filler", + "data_url":"{{entire_url('get_videos.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('videoplay.dspy')}}", + "params":{ + "id":"${id}" + } + } + } + ] + } + } +} + diff --git a/wwwroot/video/videoplay.dspy b/wwwroot/video/videoplay.dspy new file mode 100644 index 0000000..518a4b4 --- /dev/null +++ b/wwwroot/video/videoplay.dspy @@ -0,0 +1,23 @@ +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'video/{mtype}' + return { + "widgettype":"Video", + "id":"player", + "options":{ + "url":entire_url('/idfile') + "?path=" + r.mlocation, + "height":"99%", + "width":"99%", + "type":typ, + "autounmute":True, + "autoplay":True + } + }