Media Poster Space(MPS)是一个海报墙,连上自己的Jellyfin或者Emby服务器之后,会把媒体库里的海报排成一面一直在滚动的墙,用起来就像一个屏保。它有网页版和桌面版两种,网页版可以直接打开mps.controlnet.space使用,代码发布在GitHub上,项目地址。
背景
之前在《比之前更进一步的终极看番工具栈》里提到过,原来用文件夹看番的时候,没有类似于海报墙的东西。后来换成了Jellyfin[1],虽然有了海报墙,但是Jellyfin原版的海报墙实在是很丑。
Jellyfin也有不少第三方的前端和CSS主题,可以把海报墙做得好看一些,但是用下来感觉体验也就那样,还是没法脱离Jellyfin本身的那一套设计。而且有些主题对OLED屏幕不太友好,固定的元素一直显示在同一个位置,容易产生烧屏。
所以就想自己做一个可以一直运动的海报墙,海报一直在动,OLED也就不会烧屏了。背景也希望尽量黑一点,这样海报看起来会比较好看。
功能

Fig. 1. 海报墙的效果
目前主要有以下功能:
- 海报墙: 海报排成倾斜的多行,每一行都在横向滚动,左下角有一个大的时钟。剧集只显示整部剧的海报,不会每一集都显示一张。
- 防烧屏: 海报一直在移动,时钟也会定期微微移动位置,并且有呼吸的效果,不会有元素长时间停在同一个地方。
- 媒体服务器: 支持Jellyfin和Emby[2],可以选择要显示哪些媒体库。如果选错了服务器类型,比如用Jellyfin的方式去连Emby的服务器,会直接提示出来。
- 网页版: 纯静态的网页,没有后端,浏览器直接连接自己的媒体服务器。可以记住服务器地址和用户名,支持全屏。
- 桌面版: 用Tauri[3]做的,有Windows,Linux和macOS三个版本。密码保存在系统的密钥环里,还可以选择在哪个显示器上显示,也可以设置开机自动启动。
- 离线启动: 网络断开的时候,也可以用缓存的海报先显示出来,token失效或者网络断开之后也会提示重新连接。
- 纯鼠标操作: 没有任何快捷键,底部的控制栏在空闲一段时间之后会自动隐藏,只有动鼠标才会出现。

Fig. 2. 连接媒体服务器的页面,Emby和Plex收在右边,点击之后展开
使用方法
网页版直接打开mps.controlnet.space,选择服务器类型,填入服务器地址、用户名和密码,再选择要显示的媒体库,就可以进入海报墙了。
因为网页版没有后端,是浏览器直接拿着token去访问媒体服务器,所以有两点需要注意:
- 服务器需要能通过HTTPS访问。网页版本身是HTTPS的,如果服务器只有HTTP的地址,浏览器会把请求当成混合内容拦截掉。
- 服务器需要允许跨域访问。Jellyfin默认就是允许的,如果之前在设置里限制过跨域的来源,需要把
https://mps.controlnet.space加进去。
之所以不在后端做一个代理,是因为这样的话,用户的登录信息就要经过别人的服务器,反而更不安全。让浏览器直接连接自己的服务器,是更合理的做法。
桌面版可以在GitHub的Releases里下载。
如果想自己在本地运行:
1 | pnpm -w install |
界面设计
这个项目的代码基本上都是agent写的,主要用的是OpenCode加上oh-my-opencode,模型是GPT。但是GPT的模型做界面设计的能力非常弱,让它直接设计海报墙,完全做不出一个还能看的东西。第一版做出来之后,自己的评价是「丑爆了」。
所以界面的部分换了一种做法,流程如下图所示。先在ChatGPT的网页版里做一些brainstorming,把对海报墙的想法整理清楚,再把这些想法交给Google的Gemini[4]来画设计稿,其中有一部分前端是在Antigravity[5]里做的。这一步改了很多次,做出了好几个不同风格的海报墙。然后从这些设计里选一个,把设计页面保存成HTML文件,放进仓库的designs/文件夹,最后在OpenCode里让GPT参考这些设计,完成剩下的开发。AGENTS.md里也写明了,designs/里的文件只是给agent参考的,agent不能修改。
Fig. 3. 界面设计的流程,每一步之间都是自己手动传递和把关

Fig. 4. 最后选中的设计稿Parallax Horizon,里面的图片是设计时用的示例图
现在的海报墙就是从上图这个设计来的。把它交给GPT之后,第一次做出来的效果和设计稿完全不像,只好让它完全重写。后面连接页面的设计也是一样,先在Antigravity里做好设计稿,再告诉agent用这个设计稿作为连接页面的设计。
9月的时候又重做了一次连接页面,这次用的是Claude Design,同样是先出设计稿,再照着实现。三个平台用三种不同的颜色、登录页面用暗色、保留各个平台的logo、切换平台的时候加一个动画,这些都是在设计的时候定下来的。
这相当于是一个弥补GPT设计弱项的工作流: 设计交给擅长设计的模型,开发交给GPT,中间用保存下来的设计页面作为两边的桥梁。
设计稿定下的是整体的样子,还有几处是在和agent一起开发的时候自己决定的。一开始只打算做桌面版,但是既然界面本身就是网页前端,完全可以再出一个网页版,所以就要求两个版本同样的质量、同一天发布。OLED的保护一开始就定成了强保护,连海报的边缘也要能有一定的偏移,不然整齐的网格本身也是一种会烧屏的图案。点击海报打开详情的功能是自己决定删掉的,海报墙是拿来看的,没有必要去点它。结果负责最后检查的agent认为这个改动超出了原来的需求,把整个计划驳回了,最后还是按自己的意思保留了这个改动。
另外,agent有时候会伪造界面测试的证据。有一次检查发现,桌面版测试的截图其实是一张1x1的PNG,录屏是一个改了.webm后缀的文本文件,后来改成了用Playwright真实地截图和录屏。
实现
整体架构
Fig. 5. Media Poster Space的整体架构
如上图所示,整个项目是一个用pnpm和Turborepo[6]管理的TypeScript monorepo。一开始网页版和桌面版是分开实现的,一边改了另一边也要跟着改,很难统一。所以后来把界面和大部分的逻辑都移到了共享的@mps/core里,网页版和桌面版只负责和平台相关的部分,比如桌面版的密码保存和显示器选择。界面没有用前端框架,直接操作DOM。
和媒体服务器的交互都在MediaProvider这个接口后面。Jellyfin的客户端直接用fetch调用HTTP API,Emby和Jellyfin的API本来就是同一个祖先,所以Emby的客户端只是继承了Jellyfin的客户端,改了API的路径和认证的方式。Emby的支持是自己写的,写完之后再让agent检查了一遍。
让海报墙一直动下去
海报墙一开始有一个很明显的问题: 每过一秒左右,整个海报墙就会强制刷新一次。agent给了三个修复方案,但是自己觉得整个加载新海报的策略就不对,海报墙应该是一个无限的动画,不应该隔一段时间就整个重新加载。
所以就改成了维护一个海报的队列,队列里的海报少于10张的时候,就向服务器请求新的海报,补充到40张。查下来,每秒刷新的原因是诊断模块每秒采样一次的时候会调用render(),而render()会把整个容器清空再重新生成。
改完之后还有两个问题,一个是海报会重复出现,另一个是空闲8秒左右之后偶尔会整屏刷新。原因也类似,一是请求新海报的时候一直在请求第一页,二是空闲8秒自动隐藏控制栏的时候,又调用了一次render()。
最后一个问题是新海报替换旧海报的时机。自己的想法是,只有从边缘进来的海报才需要换成新的海报,已经进入屏幕的海报是绝对不能刷新的。所以现在只有每一行入口那一侧、完全在屏幕外的格子(每行最多2个)才会被换成新的海报,暂时放不下的新海报先排队等着。这样在屏幕上看到的海报,从进来到出去都不会变。
后续计划
目前支持的是Jellyfin和Emby,连接页面上已经留了Plex[7]的入口,后续会把Plex的支持作为下一个开发的目标。
总结
本文介绍了Media Poster Space,一个给Jellyfin和Emby用的海报墙屏保。它有网页版和桌面版,海报一直在滚动,不会让OLED屏幕烧屏。开发的时候,界面先用Gemini和Antigravity设计,再让GPT照着设计稿实现,算是弥补了GPT在界面设计上的弱项。
参考文献
- [1] "jellyfin/jellyfin", GitHub, 2026. https://github.com/jellyfin/jellyfin
- [2] "Emby", Emby, 2026. https://emby.media/
- [3] "tauri-apps/tauri", GitHub, 2026. https://github.com/tauri-apps/tauri
- [4] "Gemini", Google, 2026. https://gemini.google.com/
- [5] "Google Antigravity", Google, 2026. https://antigravity.google/
- [6] "vercel/turborepo", GitHub, 2026. https://github.com/vercel/turborepo
- [7] "Plex", Plex, 2026. https://www.plex.tv/