技术栈如下:

后端:Python+FastAPI

前端:uniapp+videojs

部署:deta/docker-compose+traefik

在线地址:

截图:



数据集

这里假设已经有一个数据源(),结果大致如下

[{'vod_id':'49069','vod_cid':'36','vod_name':'新双生花普通话','vod_title':'','vod_type':'','vod_keywords':'','vod_actor':'金伯莉·安妮·田舍利,詹姆斯·马','vod_director':'内详','vod_content':'内详','vod_pic':'','vod_area':'泰国','vod_language':'泰语','vod_year':'2020','vod_addtime':'2021-08-0813:12:33','vod_filmtime':0,'vod_server':'','vod_play':'dbm3u8','vod_url':'第01集$\r\n第02集$\r\n第03集$\r\n第04集$\r\n第05集$\r\n第06集$\r\n第07集$\r\n第08集$\r\n第09集$\r\n第10集$\r\n第11集$\r\n第12集$\r\n第13集$\r\n第14集$\r\n第15集$\r\n第16集$\r\n第17集$\r\n第18集$\r\n第19集$\r\n第20集$\r\n第21集$\r\n第22集$\r\n第23集$\r\n第24集$\r\n第25集$\r\n第26集$\r\n第27集$\r\n第28集$\r\n第29集$\r\n第30集$\r\n第31集$\r\n第32集$\r\n第33集$\r\n第34集$\r\n第35集$\r\n第36集$','vod_inputer':None,'vod_reurl':'','vod_length':0,'vod_weekday':None,'vod_copyright':0,'vod_state':'','vod_version':'','vod_tv':'','vod_total':0,'vod_continu':'共36集,完结','vod_status':1,'vod_stars':0,'vod_hits':None,'vod_is':1,'vod_douban_id':0,'vod_series':'','list_name':'海外剧'}]
后端
@authoryouerning加载本地数据源withopen("",encoding="utf8")asrf:db=(rf)withopen("",encoding="utf8")asrf:html=()app=FastAPI(title="SimpleVideoAPPBack",version="1.0.0",)("/static",StaticFiles(directory="static"),name="static")origins=["http://localhost","http://localhost:8080",]_middleware(CORSMiddleware,allow_origins=origins,allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)34;)vod_({"name":name,"url":url})returnvod_links@("/",response_class=HTMLResponse)defindex_page():returnhtml@("/api/search",status_code=200,response_model=SuccessResponseModel,response_model_exclude_none=True,responses={501:{"model":ErrorResponseModel}})asyncdefsearch(q:str=Query(,title="query",description="Thenameofthevideoyouwanttosearch",max_length=30)):data=[itemforitemindbifqinitem["vod_name"]]forvideoindata:video["vod_links"]=parse_vod_urls(video)returnSuccessResponseModel(data=data)@("/api/videos/",status_code=200,response_model=SuccessResponseModel,response_model_exclude_none=True,responses={501:{"model":ErrorResponseModel}})asyncdefsearch_videos(video_ids:List[str]=Body(,embed=True)):video_ids=list(set(video_ids))[:10]data=[itemforitemindbifitem["vod_id"]invideo_ids]forvideoindata:video["vod_links"]=parse_vod_urls(video)returnSuccessResponseModel(data=data)@("/api/videos/{video_id}",status_code=200,response_model=SuccessResponseModel,response_model_exclude_none=True,responses={501:{"model":ErrorResponseModel},400:{"model":ErrorResponseModel}})asyncdefsearch_video(video_id:str):data=[itemforitemindbifitem["vod_id"]==video_id]ifnotdata:returnJSONResponse(status_code=400,content={"msg":"videoiddonotexists"})data=data[0]data["vod_links"]=parse_vod_urls(data)returnSuccessResponseModel(data=[data])

我这里是直接加载的json文件,现实情况当然是使用数据库比如mysql或者mongodb,又或者全文搜索引擎如elasticsearch,meilisearch,typesense。

前端

前端不用太复杂,只要两个页面即可,一个是搜索页面,一个是播放页面.

目录结构如下:

pages└─index│└─

代码如下:

templateviewclass="content"uni-search-barplaceholder="搜索"@confirm="search"/uni-search-barviewscroll-viewscroll-y="true"class="scroll-Y"@scrolltoupper="upper"@scrolltolower="lower"@scroll="scroll"blockv-for="itemindata"uni-card:title="_name":key="_id":is-shadow="true":extra="_addtime":note="_continu"viewclass="detail"@click="()=gotoDetail(_id)"image:src="_pic"mode="widthFix"class="index-image"/imagetext{{"演员:"+_(0,30)+""+"\n\n"}}{{_content}}/text/view/uni-card/block/scroll-view/view/view/templatescriptexportdefault{data(){return{video_ids:[],data:[]}},onLoad(){varthat=({key:'video_ids',success:function(res){_ids=("获取缓存数据",_ids)if(_=1){({url:"/api/videos",data:{video_ids:_ids},method:"POST",success:(res)={=//()}})}},fail:function(err){//(err)({key:'video_ids',data:[],success:function(){('将历史记录重置为空数组');}});}});},methods:{gotoDetail(vod_id){("click",vod_id)({url:"/pages/index/detail/detail?vod_id="+vod_id})},search(value){//(value)({url:'/api/search?q='+,}).then((res)={=res[1].()//(res[])}).catch((err)={({title:'请求失败',duration:2000,icon:none});})}}}/scriptstyle/*.content{padding:15upx50upx0upx50upx;}*/.detail{font-size:30rpx;height:350rpx;display:-webkit-flex;display:flex;align-items:flex-start;justify-content:space-around;flex-flow:rownowrap;}.detailimage{width:270rpx;}.detailtext{padding-left:10rpx;width:450rpx;display:-webkit-box;overflow:hidden;text-overflow:ellipsis;word-wrap:break-word;white-space:normal!important;-webkit-line-clamp:8;-webkit-box-orient:vertical;}/style

代码如下:

代码如下:

!DOCTYPEhtmlhtmllang="zh-CN"headmetacharset="utf-8"metahttp-equiv="X-UA-Compatible"content="IE=edge"metaname="viewport"content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"title%=%/('DOMContentLoaded',function(){=/20+'px'})/scriptlinkrel="stylesheet"href="%=%static/"/linkhref=""rel="stylesheet"scriptsrc=""/scriptscriptsrc=""/script/headbodynoscriptstrongPleaseenableJavaScripttocontinue./strong/noscriptdivid="app"/div!--builtfileswillbeautoinjected--/body/html
部署

这里用两种方式部署

deta:一个免费的应用云平台,可以部署自己服务,支持自定义子域名或者使用自己的域名

docker-compose+traefik

Deta

代码请参考deta目录

非常推荐的一个小项目的部署方案,因为免费版本也就内存128mb,但是做一个demo还是不错的。

注册登录

注册登录之后会一个账号,还有一个projectkey,这里暂时用不到

安装deta

参考

创建项目
detanewvideo_site--python

创建完成之后会给一个自动生成的域名

Successfullycreatedanewmicro{"name":"video_site","runtime":"","point":"","visor":"enabled","http_auth":"disabled"}
目录结构
video_site│││││├─.deta│prog_info│└─static└─static│││├─fonts││└─

每个deta项目(Python)需要一个以及

部署

最后在项目工作目录执行

detadeploy
小结

使用deta作为demo地址还是不错的,并且还有其他服务作为交互,比如detabase可以作为一个类rediskey-value数据库使用,而detadrive可以作为一个网络空间使用。

docker-compose+traefik

配置文件太多就不粘贴了

构建镜像
mkdir-pappcp../back/*app/cp../front/app/unpackage/dist/build/h5//cp../front/app/unpackage/dist/build/h5/staticappsed-i"s/127.0.0.1/db/"app/
启动容器
docker-composeup
小结

有了docker或者说容器是的部署的环境问题得到很大的改善,环境一致干净,但是大规模的环境就需要编排工具,比如k8s,虽然编排的很快,但是网络流量处理是一个让人头疼的问题,所以Traefik应运而生,让配置反向代理变得有趣且简单,世界又变得美好了,而且Traefik支持的provider很多,比如docker,这样对于个人用户还是很友好的,如果有多个应用需要维护,可以很方便的维护这个统一的入口。

最后,Traefik跟Let'sEncrypt的配合,使得ssl证书可以自动获得,甚至是通配符ssl证书,世界变得更美好了^_^

代码地址总结

见小结。