普通视图

发现新文章,点击刷新页面。
昨天以前Ripple's blog

全新个人网站 Gamer 即将上线

2025年7月18日 10:56

前言

观察页脚不难发现,本站已经坚持了四年多了。四年间博客系统一直是Wordpress,部署在阿里云香港服务器,主题是从一款免费主题魔改折腾出的。

WordPress的这一套lamp技术有优点也有缺点,当初上手时相当容易,图形化界面就能搭建起网站,后期功能全靠插件,经典的编程语言和框架也能很快在网上找到案例分析。但想完全自定义别人的主题,折腾那套古老的PHP系统,实在有些乏力了。况且玩博客这么久,看到各路大佬们优雅的页面,难免心生羡慕,因此决定用现代的前端轮子自己造个博客。

因为博主是个无可救药的玩家,新网站被命名为Gamer,主题自然也是游戏。简单来说就是博主喜欢什么游戏元素,就缝合进去,四年里看到其他博客有什么功能/创意,也缝进去,真是“赛博弗兰肯斯坦”了。

你可以访问https://blog.hiripple.com 来新网站逛逛。

Gamer中除了搜索组件kBar,其他组件皆为自行实现,因此可能有不少BUG,目前仍处于开发中,欢迎反馈

主页

框架

Gamer的前端/全栈框架是Nextjs,后端为Supabase,部署在Vercel,对象存储为阿里云OSS。

博主并没有正经地学过前端技术,研究生期间的课题也是加密算法,因此一套流程走下来,才知道现代的框架居然已经这么方便了。例如,开发过程可以热模块替换 ,代码修改后,页面会自动更新,无需手动刷新。第一次写TypeScript,确实感受到了更好的类型安全和代码提示,也难怪自己乱写一通后,Build的报错可以修一天。

supabase

用过Supabase才感受到,原来现在搭建一个后端服务可以这么轻松,不需要配置服务器、数据库、编写后端编程语言代码、构建 API 接口、实现认证系统、设置文件存储服务,处理部署、扩容和维护等一系列操作,官方服务只需要注册个账号,自部署也可以Docker完成,动动手指一个后端即服务系统就完成了,里面的数据库、Auth、边缘函数、实时订阅等都被用在了新网站上。

部署的平台是Vercel,不愧是Nextjs的亲爹,项目一旦Push到GitHub以后立马自动部署,CDN、图片优化都是一条龙服务,方便到极致,serverless的魅力。反之在阿里云的Wordpress,连迁移都有些无从下手。

主题

或许是工地设计语言,但应该还算……个性吧。

样式

主页

主题是游戏,那博主就希望实现一个较强的“交互”,进入首页就可以看到一把大师剑缓缓浮现,它甚至会跟着你移动,是不是给用户一种即将踏上冒险、成为钦定勇者的感觉?(这样的首页虽然信息密度很低,但难道不酷吗?)

进入首页,你可以用键盘/手柄选择菜单、进入博客(还在开发中),可以用cmd + k快速搜索,还有博主从某个游戏的源文件里提取的点击音效。

找彩蛋

Gamer也有许多参考游戏设计的隐藏(指导:宫崎老贼),同时也包含了一个小游戏,找出网站全部的游戏彩蛋,老玩家可以试试?

社群

友链改名为“社群”,这个卡片样式有没有让你想起某款游戏? Gamer的大部分设计就是这么抄的。这个Rank等级可以根据互动的评论数量进行评定,一起逛逛对方的网站吧~

闪念

闪念页面的创意直接就是窃取的@skywt,其实本站很多的设计思路都受skywt佬的影响……

音频播放器

其他的各种自定义样式,都是后话了。 标题、应用、图片、代码等正文样式都会改为自己设计的复古像素风,保持主题样式的一致性。

目标

开发尾声往回看,才发现自己是一个只会“加法”,不懂“减法”的人。喜欢的东西太多,需要的功能太多,全部缝合进去,难免让整体看起来有点乱。矛盾似乎无处不在,既想要现代化的设计、动画,又想要复古的像素游戏风UI,弄成了颇具“冲击力”的组合。其次是颜色,喜欢的各种游戏主题色可能天差地别,放在一起则是灾难般的表现。

在开发中期才定了几个目标:

  • 一致的设计风格
  • 载入速度
  • 可读性

之后所有的设计思路都必须满足三个目标,特别是可读性。一个博客最重要的还是内容,因此不能让花里胡哨的样式影响到阅读。

可切换的字体

为了保持设计风格的一致,字体还是默认选择了像素字体:缝合像素字体。但考虑到可读性,就在博客加入字体切换功能了,阅读时可以自行切换到楷宋,并且浏览器可以记住你的选择。

目录

最初只是觉得,一把缓缓浮现的大师剑很酷,但没考虑可读性的问题。请问一把莫名其妙放在左边的剑,到底是想干什么?

那么要做的就是赋予它在阅读上的功能,同样是参考了某款独立游戏,加上了“黑潮”,使其成为目录。

速度的优化也费了不少功夫,例如动态载入、服务端组件和客户端组件分离、各种压缩等,最后把gtmetrix成功刷到了100%,谷歌page speed也有97左右,速度应该还不错。

其他的一些细节包括非首页自动展开、记住用户选择等,后面等边看边修吧。

颜色

统一的主题色可以让网站看起来更和谐,早期的Gamer为了缝合更多的游戏,页面五花八门。最后的阶段开始做“减法”,将主题色定为 Oneshot 紫 + Zelda 绿,还有常规的黑白。没错,博主不懂设计,这些色调都是用取色器从游戏中偷的😊。

日/夜间模式

日夜间模式切换一直是我期望实现的目标,引用Skywt的一段话:

让网站支持 darkmode 是我的一个执念。因为:1)所有浏览器、操作系统都有了 darkmode 的功能,如果不去兼容这个功能,会感觉自己的网站是「功能残缺」的;2)我既想要纯白的简洁设计,又想要在被窝里看着不伤眼睛的暗色设计。同时做两套主题能够满足我这样的要求。

然而 darkmode 设计和实现起来并不容易。为了保证颜色的协调,往往需要对两套主题单独调整颜色,并不是简单的「反色」。

Gamer的思路也类似,为两个主题设计了两套颜色(日间模式是KOJIMA灰哦)。至于切换主题的方式,因为找不到合适的位置,于是就做成了可拖拽的工具栏……

i18n

国际化

此前Wordpress博客的另一大痛点就是国际化,想在原主题上加i18n,那会是非常大的工程。好在Nextjs的路由系统可以很方便地加入国际化,配合自带的无刷新载入,可以实现在路由层面快速切换语言,满足高性能和SEO的需求。

正好目前在学习日语,博客因此准备了中日语三语切换,目前还是机翻,后续会人工翻译/核对,但也不能保证全部博文的国际化。

小结

hdr/livephoto

Gamer其实还有很多没有提到的功能,例如realtime、实时状态、新评论区等,细心的话用谷歌浏览器的话,还可以发现本站的UI是支持HDR的。HDR一直是博主非常喜欢的一个特性,毕竟光线是视觉上最重要的要素,高亮度和对比度让数字世界更接近真实世界。但新网站还没有全面拥抱HDR,因为万恶之源Safari(HDR组件在不支持的浏览器上效果太差了)。

最后,hiRipple的Wordpress博客将会逐步导出博文、评论,搬迁到新地址。新网站逐步完善后也会在GitHub开源。

全新个人网站 Gamer 即将上线最先出现在hiRipple

V50,组一个5.1环绕声道

2025年4月2日 20:09

这两天刷B站,发现了一个相当魔幻的数码产品。

以高达形态出击

眼馋家庭影院的物理环绕许久,但租房在外,整个硕大的音响阵列和功放,搬家会变成地狱难度。因此说实话,这个软广看着还真有点意思,做的思路是独一份,但无奈售价幽默,几个小小的喇叭居然敢定价2k+,完全可以组一套正经杜比全景设备了。

前段时间才花重金购入漫步者a80,眼下得消费降级,况且环绕声最大的应用是游戏和电影,和HIFI搭不上边,所以博主挑选音响的目标是:能响就行。

5.1环绕

5.1环绕声由 前置左/右(FL/FR)、中置(CNT)、后置左/右(SL/SR)和低音炮(LEF)六个音响组成,其中最重要的当然是前置左/右和后置左/右,中置可以使用FL和FR模拟,低音炮仅仅作为锦上添花,只要有前后音响就能构成环绕的感觉。

小度

直接把手头上空闲的小度便携版利用起来(音质一言难尽)。

voicemeeter 虚拟声卡

有了音响,那么下一步就是将他们组成环绕声,这里用的是voicemeeter banana,是个免费软件,可以神奇地将各种音频输出整合在一起,包括HDMI、蓝牙、耳机孔等。将A1、A2通道分配给HDMI和小度,再将小度设置为rear only即可简单地实现环绕声了。

不过虚拟声卡有不少缺陷,首先自然是稳定性问题,想实现环绕需要后台持续运行软件,也占用了额外的性能开销,其次就是延迟问题,Voicemeeter不能很好地统一各个设备的延迟,尤其是蓝牙搭配有线的情况。

Realtek audio console

为了进一步提升,那一个办法就是购买外置声卡和更多地有线音响,不过博主很幸运地发现,微星主板自带了一个还不错的声卡,同时支持3个后面板3.5mm插槽和2个前面板插槽,足以接上7.1声道的全部音响,免去了外置声卡的开销,并且由于是物理声卡,也不会占用额外的性能,不过能组阵列的也仅限和声卡直接连接的音响了。

既然都到这一步了,不妨把该买的都买了,博主直接从HNU同学那50收了一套飞利浦spa2341 2.1音响,一个低音炮和两个卫星箱。为什么选这个呢? 除开价格便宜外,最重要的是这套飞利浦音响的两个卫星箱是有源的,这意味着可以同时将它作为低音炮和后置左右声道来使用,一石二鸟。

4英寸(没参数,猜的)的低音炮效果比A80好上不少,小但也够用,两个后置卫星箱体积迷你,声音还算可以入耳,作为环绕的辅助设备足矣,将它们接上PC,几乎完美。 在Realtek管理面板中,只需要把中置喇叭取消勾选,那么收听5.1音频时,中置声道会由前置左右音响模拟,效果几乎没区别。

音频输出
属性

接下来就是游戏和电影的实际应用,体验后只后悔为什么没有早点花这50,整体的音频效果可以说提高了一整个level。现在几乎所有的电影和剧集都是5.1声道起步,进入potplayer将声音输出改为源作为输出,然后开始享受吧。

东大寺

至于游戏,大多数3A都已经支持环绕声,包括最新的《刺客信条:影》,大风刮过的沙沙声、NPC在背后的议论声、悠扬的陶笛声等等,真正进入了游戏的世界。

等成为社畜后,一定要折腾出一套真正的7.1杜比全景声,让NS2和PS5也体验到环绕的感觉。

V50,组一个5.1环绕声道最先出现在hiRipple

开箱M4 MacBook Air 乞丐版

2025年3月12日 20:56

赶上首发啦~

虽然半年前才换了M2 Pro 14寸,但新Air价格也太香了,国补+教育优惠5799的价格,谁忍得住。发售当天博主直接跑到国金提了一台银色 M4 Air,也算赶上一次Apple的首发。

库克如此良心的首发还是第一次见,加量不加价,主要可以概括为:升级M4芯片,16G内存起步,2个雷电3升级为雷电4,前置超广角摄像头,并且起步价-1000 。

经典的果味道包装
乞丐版万年的30w充电器,厨子的刀法

M4作为Apple进军ARM的第四块芯片,架构已经相当先进了,能耗比做的相当棒,因此博主也希望这台新Air可以想iPhone 13Pro一样,当个钉子户。

纠结了非常久的颜色,最开始下单的是星光,但看评测似乎有随时转变为土豪金的风险,因此最终选择了百搭又经典的银色。

正面照

银色相比深空灰,有个很有趣的地方,暖光下会变成星光色,而冷光下又偏白。

暖光
左暖右冷

其实针对MacBook这个品类,博主还是更喜欢Air,Pro系列的厚度与重量基本已经和“精致”与“便携”背道而驰了,而需要极高性能释放的任务,我更倾向于使用台式机或者远程完成。重新拿回Air的第一感觉就是“太薄了”,简直是艺术品,不过另外意外的是,两者的屏幕厚度倒是相差无几,毕竟Pro可是峰值1600nit的Miniled屏幕啊。

除开尺寸,Air续航比Pro系列芯片会更胜一筹,屏幕分辨率下降,变为60帧普通LCD。博主认为这些并不是致命问题,一方面MacOS动画远少于IOS,低刷感知并不强,HDR的应用最主要在照片、流媒体、电影这一块,完全可以通过外接显示器解决。

厚度天壤之别
屏幕厚度

除开屏幕,Pro的一大亮点就是笔记本中最顶级的音响,不过博主大部分时间都是外接漫步者A80听音乐的,反而Pro音响细密的开孔应该改进一下,极易吸灰影响美观,反观Air就没有这个问题。

扬声器开孔

此外不得不提的一点就是,Air的键盘手感确实差了不是一点。由奢入俭难,当初Air换Pro居然没什么感觉,现在拿回Air的第一感觉就是键盘手感相当奇怪,具体来说,Pro的更有质感,Air太过清脆。

双色对比
银色真不错

最后吐槽下Apple的迁移助手,如果没有雷电线请务必避开这天坑App,老老实实手动复制文件,不然等着你的可能是无尽的进度条循环。

开箱M4 MacBook Air 乞丐版最先出现在hiRipple

7800XT -> 4070TI S,为什么我抛弃了A卡

2025年2月11日 19:13
7500f+6750gre,记录人生中第一次装机 - hiRipple
使用了四年的宏碁暗影骑士1660ti,终于到了下岗的时候~
hiripple.com

续上文A卡装机,从各路数码博主评测购入6750GRE,到升级7800XT,本以为能让PC Gaming体验更进一步,实际上却不尽人意。博主自认为是一个很会“折腾”的人,并且6000系也经历了几个月的正常使用,这些都让我对A卡信心大增,但7000系却给了当头一棒,包括但不限于游玩过程中频繁黑屏(突发黑屏几秒后恢复),休眠唤醒绿屏/花屏,VR串流视频编码的高延迟,甚至部分游戏连光影都是错乱的(例黑悟空,换驱动后才知道,游戏原本并没有这么黑)。其它小毛病例如相比N卡的高功耗、性能监视器不可用,瞎眼FSR和摆烂几个月也不更新的驱动,种种毛病几乎让博主忍无可忍,最终加钱2.5k换上了同显存的4070TI S,几乎解决了上述所有毛病。

单7000系而言,AMD显卡驱动几乎到了不可用的地步,“游玩过程中频繁黑屏(突发黑屏几秒后恢复),休眠唤醒绿屏/花屏”这两个问题在更换数个驱动后,仍然不断出现,一度让博主以为是内存/CPU发病,一番折腾排查、各种重装焦头烂额,在显卡返修之前按照售后的说法,原来是驱动回退的不够久。实在很难想象,是什么样的草台班子在维护它。

本文就简单聊聊博主换上N卡感受到的其它优点,聊聊为什么无论是小白还是老登,都不再推荐A卡,有些东西实在不是一个价格就能弥补的。

开箱微星4070TI Super expert

很有质感的一张卡

expert系列虽然散热比旗舰差了些,但耐不住仿公版的顶级外观,双滚珠轴承风扇几乎没有啸叫,全铝合金双风扇相比其他显卡真是“出淤泥而不染了”。

1、 RTX_HDR

一向鲜有提及却效果惊人的功能,同时也是博主最喜欢的功能。博主对HDR有着特别的追求,这也是如此钟爱Apple生态的一大原因,换上MacBook Pro、Miniled显示器与电视都是为了观看HDR内容。即便WIN11的HDR有了不小进步,但仍然和主机间有着不可忽视的差距,auto_hdr功能仅能为部分不支持HDR的游戏加上滤镜,而RTX_HDR可以为所有游戏都映射HDR,并且Ai加持后的效果也更接近于原生HDR。更进一步,RTX_HDR不仅仅支持游戏,还可以转化SDR视频,配合上超分功能,对于SDR的老游戏和电影,以及没能力做HDR适配的开发者(还是黑悟空),可谓是焕发第二春。总结,对于喜欢HDR的玩家,大可直接跳过A卡。

下图放出最近在玩的银河恶魔城新作《终焉玛格诺利亚》原生SDR和RTX_HDR的对比图,Auto_hdr不支持此游戏,注意观察座椅上方的光线。ps:请用支持HDR的设备观看下图,格式为AVIF HDR。

RTX_HDR

SDR

这里吐槽下Apple Safari,居然现在都不支持网页预览HDR图像,无论是AVIF、PNG还是JPG全部只能显示SDR,因此不得不使用MP4 HDR体现效果。

2、VR视频编码

另一个同样鲜有提及却不容忽视的就是视频编码能力的差距,这一点最主要体现在VR串流中,一体机的VR和直连的PCVR不同,串流需要显卡将渲染后的画面按照特定的格式进行编码,例如H264、HEVC、AV1等,编码后传输至一体机进行解码,因此串流对于编码的延迟要求相当高,最终的结果是,7800xt串流的画面时长出现卡顿与高延迟,这一点更换至4070TI Super后有一定程度的缓解,但卡顿的现象仍然存在(怀疑是一体机性能不足)。当然对于目前用的500元4k头显,能偶尔光剑启动下就已经满足。

3、transformer模型

GitHub - beeradmoore/dlss-swapper
Contribute to beeradmoore/dlss-swapper development by creating an account on GitHub.
github.com

相比A卡的另一个巨大提升就是DLSS中的超分功能了,DLSS4新增了transformer模型,超分质量得到了巨大 提升,并且可以强制应用于支持老DLSS的游戏,使用以上链接的工具替换DLL文件即可。

4、更新驱动解禁!

为了让Windows不自动更新A卡,此前还将组策略编辑器中的显卡设备安装行为全部禁止了,现在终于可以下载到最新的驱动~

5、功耗与噪音降低

功耗降低接近一半,一年下来或许能省不少。

6、光线追踪

即便皮衣黄大力鼓吹光线追踪,博主觉得也没必要为此补上与A卡之间的差价。对于4K屏幕,4070及以下的显卡开启光追后基本很难维持住帧数,即便性能够也难以避免爆显存,毕竟老黄的显存可是比苹果的内存还寸土寸金。此外支持光追的游戏并不多,除开特定游戏的提升也不够显著,整体而言优势并不大。

当然到了中高端卡的价位,可以不用,但不能没有。下面是Portal_RTX在最高光追+DLSS质量+RTX_HDR下的表现,确实比原版提升很大。

7800XT -> 4070TI S,为什么我抛弃了A卡最先出现在hiRipple

hiRipple 2024年度总结

2025年1月6日 17:02

一转眼一年又过去了。24年是意义非凡的一年,这一年博主正式成为了湖大研究生,同时也发生了太多太多难忘的事。

RGA2024

年度游戏:《博德之门3》

提名如下:

  • 《博德之门3》
  • 《黑悟空:神话》
  • 《黄金树幽影》
  • 《宇宙机器人》
  • 《暗喻幻想》
  • 《寂静岭2重制版》
  • 《帕特里克的箱子无穷奇遇》
博德之门

注:RGN年度游戏入围标准为Ripple本年游玩过的作品,并非本年发售的作品。

TGA23年年度游戏,RGA24年年度游戏《博德之门3》。曾几何时,博主作为老任豚,对于《王泪》的落败是相当不服的,直到今年博起100小时后,一切都发生了变化。首先聊聊开发商拉瑞安工作室,从破产边缘到用硬实力吸引威世智获取授权,从最初简陋的林地到无穷自由度的庞然大物,在玩家的意见中不断改善,即便正式版发售后也源源不断地更新,他们是真正热爱的游戏的工作室,开发游戏并非为了袈裟奖项,印证了什么才是“Game Developed By Love”。回到游戏质量上,即便任豚也不得不承认,博德确实有资格击败王泪,站上年度游戏的舞台,一周目100小时可怕体量,几乎全对话配音的用心程度,以及各种各样等待开发的路线以流派,这是一款非常罕见的,让我在一周目游玩过程中就开始对二周末浮想联翩的作品。

游戏业发展至2024年,3A作品确实有着一股趋同的趋势,不少作品陷入画面精美、玩着无聊至极的怪圈,特别点名“开放世界”这一类别,博主眼中的开放世界,要么做到顶级(《巫师3》《荒野之息》等),要么就是玩不下去的罐头(《地平线》《消逝的光芒》等),而神作的数量自然远少于罐头,因此博主也在很长一段时间对所有开放世界都非常抗拒。而《博德之门3》正好属于顶级的开放世界,每一个任务都精心打磨、玩家自由选择走向,网状叙事总能在未来出现意想不到的惊喜。其他例如战斗、音乐、画面表现等,全都站在当今游戏行业的顶尖水平。实话说,本作如果能将第三章真正完整地做完,那无疑将列如RGN 10/10的无暇神作。

此外,本作也向博主科普的大量DND及跑团知识,《龙与地下城:侠盗荣耀》也非常好看~

年度独立游戏:《山河旅探》

提名如下:

  • 《帕特里克的箱子无穷奇遇》
  • 《小丑牌》
  • 《以撒的结合》
  • 《动物井》
  • 《灵视异闻:本所七大不可思议》
  • 《山河旅探》
  • 《历历在目》
  • 《霓虹白客》
  • 《潜水员戴夫》

《山河旅探》作为今年年度独立,虽多少吃了点“国产Buff”的加持,但其质量本身也属实过硬。由于故事背景的发生时间相近,甚至有种在玩《大逆转裁判》的感觉,官方在游戏结束的字幕中也明确向《逆转裁判》《弹丸论破》等作品表示致敬。最终给博主的感觉是,致敬但并非抄袭,将中国近代史和推理案件很好地结合,称其为国产之光并不为过,很期待未来国内能有更多优秀的作品。

解释下其它提名作品,《动物井》在TGA之后呼声最高,博主也打出了白金奖杯,即便本作素质确实不错,但实在没对上博主的胃口,并且博主很怀疑吹嘘动物井的人群中,究竟多少亲自玩过了游戏,而不是b站看了下本作的精华视频就盲目跟风。一方面,《动物井》操作难度相当高,博主此次打通蔚蓝A B面和一半C面,自认为2D平台跳跃有个中上水准,但本作却一度让我破防,不仅操作要求严格,而且死亡后遥远的复活点带来的负反馈也令人相当难受。另一方面,博主从来不认为“能藏就是好游戏”,藏了多少东西,藏的多深从来不应该是值得吹嘘的点,隐藏的内容配合上好的设计和引导才是真正的宝藏,而对于动物井,个人认为藏的太多,引导又太薄弱。

如果之前没有玩过《以撒的结合》,那么它铁定是今年的年度独立,本作放在提名中纯粹是今年重新拾起仔细玩了,并且确实是肉鸽品类的顶点。而TGA的年度独立《小丑牌》,很遗憾只是初见上头,全赌注通关后几乎没有再打开过,作为肉鸽,限制小丑(遗物)的数量实在难受,这一点《杀戮尖塔》《以撒》就做的非常好。

最佳音乐/配乐:《博德之门3》

提名如下:

  • 《博德之门3》
  • 《寂静岭2》
  • 《暗喻幻想》
  • 《最终幻想7 重生》
  • 《女神异闻录3 重制版》

《博德之门3》又拿下了RGA的另一奖项,100小时之后,本作的OST也丝滑地进入了歌单。《Song of Balduran》《Weeping Down》《Power》《Raphael's Final Act》简直余音绕梁,真想去费伦大陆当一名吟游诗人。


RMA年度影片:《上帝保佑美国》

hiRIpple今年新开了奖项:Ripple Movie Awards。入围标准仍然是本年度内Ripple欣赏过的电影,那么下面介绍一下2024年度影片。

God bless America

“他们挺有才是因为,和一群令人绝望和困惑的人站在一起,但我向你保证,他们没有丝毫才能。”

“兄弟,我敢说3200万人都不会同意你的看法,因为去年就有这么多人在最终决赛进行了投票”

“我真希望我是个天才发明家,可以发明一种电话,里面装有爆炸装置,只要拨打“美国巨星”电话就会触发,电池就会爆炸,在脸上留下标记,我就可以在他们开口之前知道是谁。“

“你就在现场,亲身经历过,这种体验还不够吗?,我是说,下次你想记住什么事,不要拿出手机记录,为什么不用自己脑子呢记住?“

“我真讨厌这个国家”

“所以我们才要去法国”

连博主自己都很难想象,年度影片会属于一个不常看的类别(喜剧),七开头平平无奇的评分,可能对上胃口真的很重要。同样是大叔+萝莉组合,本片在博主心目中超过了《这个杀手不太冷》《火柴人》《孤胆特工》等,没有冗长枯燥的说教,真正做自己想做的。男主说出来太多博主的心声,也干了太多博主不敢做的事,例如对着电影院大吵大闹的逆天来一顿扫射,对着乱停车的痞子直接崩上一枪,在很多价值观上,博主和男主一样比较传统,即便有批评的声音指着导演的普世价值,但博主觉得虚头巴脑讲一堆道理,远不如看完电影酣畅淋漓的感觉来的痛快。

其他推荐影片:

  • 《某种物质》
  • 《因果报应》
  • 《荒野机器人》
  • 《末代皇帝》
  • 《头脑特工队2》
  • 《险恶》
  • 《机器人之梦》
  • 《你想活出怎样的人生》
  • 《盗火线》
  • 《怪物》
  • 《天国王朝》
  • 《勇敢的心》
  • 《角斗士》
  • 《虞美人盛开的山坡》
  • 《全金属外壳》
  • 《变脸》

附:RGN全年评分

PS5
游戏平台

Strm+Emby=无痛刮削,应对网盘风控新策略

2024年12月29日 15:27

前言

NAS折腾记1️⃣:从OpenWrt到Unraid - hiRipple
All in One Or All in Boom?
hiripple.com

此前一篇博文中,博主采用了115+alist+infuse实现了简陋的家庭影院系统,但随着近期阿里云盘的崩溃,大量用户涌入115,官方为了降低服务器压力,开始封禁第三方挂载的刮削/扫库行为,目前infuse扫盘一次就会出现“429 too many request” 。

封禁扫库行为

虽然Dps限制可以通过并发线程数和修改分页大小,但这种方法治标不治本。限制太极端,导致infuse启动后需要等待相当长的时间才能获取完整的媒体库,限制不足又将导致频繁429,甚至变成永久封禁。归根到底,还是得解决infuse的暴力扫描问题。

Fnos

近期国产NAS系统Fnos爆火,飞牛影视作为该系统的扛把子功能,可以较为准确、快速地刮削元数据并对接infuse,最厉害的还是可以刮削更符合国人习惯的豆瓣元数据。博主在Unraid虚拟机上尝试了Fnos,虽然一定程度解决了刮削问题,但蓝光原盘ISO居然被直接无视了,咨询官方才知道不支持原盘,pass

MetaShark

下一位选手是老朋友JellyFin。据了解,JellyFin目前已支持蓝光原盘的扫描,并且还有一个很棒的刮削插件MetaShark(刮削豆瓣元数据),媒体服务器无法对AList提供的Webdav共享直接扫描,因此需要用Fuse将Webdav挂载为本地文件夹再导入媒体库。实测JellyFin导致115出现429封禁的概率是百分百,而且扫库速度奇慢,pass

折腾大半天后,博主甚至已经起了购入机械盘和正经NAS的想法,但本文的主角来的正是时候~

文章测试环境:

  • 底层系统:Unraid
  • Docker:Emby、Auto_symmlink
  • 可执行文件:Clouddrive
  • 可选:Alist+Fuse
  • 客户端:AppleTv+infuse
  • 网盘:115

配置Strm环境

什么是Strm文件

正文开始之前,介绍一下什么是Strm文件,简单来说Strm就是一个软链接,指向一条通往真正媒体文件等路径,这个路径可以是直链,也可以是本地路径。

创建一个普通文本文件并将.txt 扩展名重命名为.strm。然后使用文本编辑器(如 Microsoft Windows 中的记事本)打开它并输入流的直接 URL 链接。

这应该看起来像:

http://192.168.2.1:567/movie/spirited_away.iso

或者

mms://host/path/stream

或者

rtsp://host/path/livestream/cctv1.m3u8

或者

F:/Movies/Topgun (1986)/Topgun.mp4

Strm 文件可用于任何类型的视频,例如电影、剧集、音乐视频、家庭视频等,只需将 .strm 文件放在您想要的位置,就好像它是视频文件一样。

为什么要生成strm文件?

因为115的风控规则导致无法批量扫描和刮削视频文件,这时strm远程链接的特性就发挥了作用,刮削软件可以把strm视作视频文件,根据文件名获取信息,而无需去115读取文件,并根据需要改名而保持链接还是指向正确的远程地址。而获取115的文件列表只是通过读取目录信息而并不读取文件,所以更准确的说是扫描目录而不是扫描文件。

进一步来说,Strm避免了媒体服务器/infuse对网盘中媒体信息(例如分辨率、HDR、时长、音轨等)直接进行刮削,通常来说对于ISO原盘文件,刮削使用的ffprobe需要相当高的带宽资源才能提取出信息,这也是导致封控的罪魁祸首,如果媒体服务器(Emby)扫描的是Strm软链接,则不会使用ffprobe,只会简单从互联网刮削电影元数据(例如电影名称、海报等)。

这么做的最大好处自然是避免封控,所有刮削操作所需要的目录、电影名称都保存在本地,弊端自然就是无法提取媒体信息(Emby如果没有时长数据,将无法同步播放记录)。好消息是,无论Mkv还是ISO原盘,都存在曲线救国的解决方案,请看后文。

生成Strm文件

对媒体库手动添加Strm是不现实的,因此需要自动化工具实现。因为博主讲Emby部署在本地,不需要考虑流量消耗,这里选择较为方便的本地路径生成Strm。

注:互联网上其他教程也提到了通过Nginx转发实现302重定向,不消耗服务器流量,外网流畅观影。除非实在必要,否则博主不推荐这种做法,一方面部署302需要部署更多容器,更复杂的操作也加大了维护成本,另一方面115官方一直严格限制302挂载,因302被封禁账户下载与在线播放权限的例子并不少见(failed link: failed get link: {"state":false,"msg":"账号存在异常,此功能已被停用","errno":990020,"data":""}: unexpected error),猜测是检测多IP同时302下载,总而言之,最安全的做法还是放弃302。

既然需要指向本地路径,那就必须先把之前用的Alist Webdav挂载至本地,通常的方案是使用Fuse(Rclone):

curl https://rclone.org/install.sh | sudo bash
rclone config # 按照提示添加Webdav存储

rclone mount mywebdav:/ /mnt/webdav --daemon --vfs-cache-mode writes --allow-other # 挂载Webdav到本地
ShellScript

挂载之后访问指定文件夹,应该就可以看到网盘中的文件了。注意,为避免风控,Alist应针对115存储进行相应的限制,推荐的配置是:1、更新Alist为最新版本。2、分页大小:9999,限制速率(限制所有 api 请求速率(1r/[limit_rate]s)) :1。

博主不喜欢装太多的轮子,因此选的是另一套方案:Clouddrive,虽然本质上也就是Alist+fuse的缝合,但整合在一起并且提供GUI界面还是不错的。值得一提的是,CD并未开源,并且收费,膈应的朋友可以选择前者,否则还是更推荐Clouddrive。

docker run -d \
    --name clouddrive \
    --restart unless-stopped \
    --env CLOUDDRIVE_HOME=/Config \
    -v <path to accept cloud mounts>:/CloudNAS:shared \
    -v <path to app data>:/Config \
    -v <other local shared path>:/media:shared \
    --network host \
    --pid host \
    --privileged \
    --device /dev/fuse:/dev/fuse \
    cloudnas/clouddrive2
ShellScript

Clouddrive通常使用两种安装方式:直接下载编译好的可执行文件/Docker容器,Docker的部署需要额外的步骤并且需要映射路径,博主选择的是前者。运行后访问http://localhost:19798,随后按照GUI界面将网盘文件挂载到本地。

设置目录缓存持久化

为避免风控,CD同样需要进一步设置,推荐的配置如下:1、更新CD至0.8.6版本以及上。2、设置勾选目录缓存持久化,默认目录缓存时间:1800。

Auto_symlink:自动生成Strm文件

既然已经把网盘文件挂载到本地,那么下一步就是针对每一个视频文件生成对应的Strm文件,并且在网盘目录发生变动时,自动添加/删除对应的Strm文件。部署Auto_symlink项目来实现这一目标。

docker run -d \
  --name auto_symlink \
  -e TZ=Asia/Shanghai \
  -v /volume1/CloudNAS:/volume1/CloudNAS:rslave \
  -v /volume2/Media:/Media \
  -v /volume1/docker/auto_symlink/config:/app/config \
  -p 8095:8095 \
  --user 0:0 \
  --restart unless-stopped \
  shenxianmq/auto_symlink:latest
  
# -v /your/cloud/path:/cloudpath:rslave: 将你的云盘路径(/your/cloud/path)映射到容器内的路径(/your/cloud/path)。rslave 表示使用相对于宿主机的从属挂载模式。请确保左右路径保持一致,否则生成的软链接不是指向真实路径,导入emby中的时候会导致无法观看。(简单的来说,这里需要填写你映射的云盘路径,且两边都填写一模一样的路径即可。)
# -v /your/media/path:/media: 将你即将创建软连接的位置映射到容器内的 /media 目录。
# -p 8095:8095: 映射8095端口,可方便的查看日志以及管理服务。
# -v /path/to/auto_symlink/config:/app/config: 将 auto_symlink 的配置目录映射到容器内的 /app/config。这样可以使容器中的 auto_symlink 使用外部的配置文件。
# --restart unless-stopped: 设置容器在退出时自动重启。
ShellScript

注意部署该项目时,需要特别注意映射路径,因为涉及Strm文件的路径构成,映射错误可能导致后续Emby无法找到文件。对于媒体文件目录,一般将宿主机路径和容器路径保持一致,对于Clouddrive根目录,也进行相同的映射,最后映射Appdata即可,博主的实例如下:

运行后访问http://localhost:8095 即可进入WebUI。首先进入全局设置,推荐开启挂载检测、打开同步状态与实时监控。实时监控功能可以与Clouddrive联动(需要会员),在CD的Webui上存入/删除文件后,本项目可以立即同步。

实时监控生效的条件如下:

  • cd2会员
  • 文件是通过cd2挂载文件夹/网页版cd2中操作的,在网盘app中操作无法触发实时监控
  • 检查是否打开全局设置中的实时监控,开启后重启AS
  • 查看日志,看看是否有"开始监控xxx文件夹的作用",如果只出现"开始索引xxx文件夹",则说明该文件夹文件太多,索引时间很长,建议开启永久缓存
  • 如果实时监控一直处于索引文件夹的状态,可能是因为系统监控文件数受到限制,可以依次运行下面三行脚本后,重启AS即可:
sudo echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf

sudo echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf

sudo sysctl -p
ShellScript

随后在主页添加同步,媒体目录为网盘挂载在本地的目录,本地目录为保存Strm文件的目录(Emby媒体库目录),建议勾选更新软链接/删除软链接,确保CD2的操作同步,元数据相关的选项全部跳过,为防止风控,全部元数据以Info、json、jpg等格式保存在本地,不涉及云端,因此可以跳过。全部设置如下:

随后前往-常用工具-手动同步,开始第一步的数据同步(如果读取目录为空,应第一时间采用重启大法)。同步成功后前往strm保存的目录,下载任意strm文件并用记事本打开,核对路径是否与网盘媒体文件路径一致。不一致就是路径映射错误,重新配置并同步即可。

推荐配置:常用工具-Emby通知-填写Emby地址与APIkey,勾选启用通知。启用此工具后,每当Auto_symlink生成/删除strm文件时,将自动通知emby重新扫描媒体库(Emby可以直接关闭定期扫库)。最终实现的效果是Clouddrive转存视频文件,Auto_symlink自动生成对应的strm,emby自动刷新媒体库,非常方便快速。

常见问题:

Q: EMBY显示当前没有兼容的流

: 请确保你EMBY映射的也是绝对路径,需要与 auto_symlink设置的路径保持一致。也可能是Emby转码问题,尝试使用infuse播放。

Q: 虽然我有元数据,但EMBY扫库还是很慢?

: 因为我们映射了所有影片的软连接,所以可以尝试先禁用EMBY的FFmpeg进程,CloudDrive2可以在设置黑名单添加/bin/ffprobe,扫库完成后,再删除该黑名单即可

正确打开Emby

配置代理

第一次体验Emby时,感受到最大的就是奇慢无比的刮削速度,给博主的印象是一款没有啥优势的媒体服务器。与Plex竞争,缺失了一键内网穿透、plexamp音乐库等功能,而对比开源的JellyFin,插件生态薄弱,硬件加速价格昂贵,Strm+Emby组合似乎打开了新世界的大门(JellyFin不支持Strm,貌似是支持的)。

如何解决刮削缓慢的问题呢,emby元数据来自themoviedb,而tmdb在国内环境受到严重DNS污染,早些时间互联网其他教程推荐修改本地Hosts,手动解析至对应IP,目前这种方案已经几乎不可用。因此想达到一个正常的刮削速度,就必须为Emby配置代理

配置代理

博主在Unraid上部署了Openwrt虚拟机,使用Passwall2配置了Http代理,想让Emby走Openwrt代理,需要在Docker容器启动时添加对应的变量,如上图所示,填写Openwrt地址以及端口。

Emby的媒体路径映射同样需要注意,媒体文件路径最好保持宿主机路径与容器路径一致,因为Emby打开strm链接时,是以容器的角度搜索的,如果不映射完整的路径,依旧无法找到媒体文件。

最后访问http://localhost:8096即可进入Emby的web界面,添加媒体库,选择Strm对应的目录,扫墓媒体库文件,精美的海报墙就出现了。

海报墙

刮削Mediainfo

缺失Mediainfo

但是到此还远没有结束,即使有了电影元数据,媒体信息也并未提取,无论emby网页还是Infuse,都无法预览影片的相关参数,这导致的问题如下:1、不美观。2、缺失时长信息,infuse重启后播放进度丢失。3、载入影片的时间变长。

为了刮削mediainfo,博主采用了两种方案,对于MKV文件,只需要安装Emby插件:StrmAssistant,传送门。注:Infuse官方推荐的Infuse sync插件不建议安装,一方面会与StrmAssistant产生冲突,另一方面由于影片元数据全部保存在本地,并不需要优化同步速度,况且目前基本都已经是直连模式。

StrmAssistant最大的作用就是替代Emby对MKV影片进行Mediainfo的提取,“独占媒体信息提取”可以禁用Emby自带的ffprobe,并采用更低频率的ffprobe提取Strm对应的影片信息,既防止风控又可生成媒体信息,对于剧集还可以探测片头长度提供跳过,优化载入速度。安装插件-重启Emby-计划任务-执行神医助手任务,即可生效,此时对于MKV文件已经可以正常记录播放进度。

遗憾的是,StrmAssistant并不支持ISO文件,因此提取ISO原盘的Mediainfo就成了最困难的一步,只能自己动手,用脚本实现。第一个思路是直接用Mediainfo项目,无奈也不支持ISO,那么还是得安装FFmpeg。

Unraid这羸弱的性能就不指望从源码编译了,从https://github.com/BtbN/FFmpeg-Builds/releases下载对应的Build,解压进入bin目录就可以调用ffprobe进行手动提取了。注意提取蓝光ISO信息的指令与常规视频文件略有区别,ISO路径前必须加上bluray: 。例如想提取[岁月的童话 1991][台版原盘 国粤双语 DIY简繁 双语字幕].iso,可以执行:

    ./ffprobe -v error \
        -print_format json \
        -show_format \
        -show_streams \
        -show_chapters \
        -show_programs \
        bluray:"path/to/[岁月的童话 1991][台版原盘 国粤双语 DIY简繁 双语字幕].iso" > "[岁月的童话 1991][台版原盘 国粤双语 DIY简繁 双语字幕]-mediainfo.json"
ShellScript

在UNraid系统下直接调用ffprobe会出现警告:bdj.c:795: BD-J check: Failed to load JVM library
bdj.c:795: BD-J check: Failed to load JVM library。这是缺失JAVA运行环境导致的,想消除警告,前往https://jdk.java.net/23/下载对应的OPENJDK,将JAVA导入系统路径并重新执行ffprobe,会惊奇地发现,居然生成了更多的警告:

 ffprobe bdj.c:614: libbluray-j2se-1.3.4.jar not found.bdj.c:801: BD-J check: Failed to load libbluray.jarbdj.c:614
 bdj.c:632: Cant access AWT jar file /usr/share/libbluray/libbluray-awt-j2se-1.3.2.jarbdj.c:801: BD-J check: Failed to load libbluray.jarbdj.c:632: Cant access AWT jar file /usr/share/libbluray/libbluray-awt-j2se-1.3.2.jarbdj.c:801 
 ...
ShellScript

这是因为安装JAVA环境后,又缺失了相应的依赖,博主在这边直接提供编译完成的JAR文件。

https://772123.xyz/cdn/libbluray-awt-j2se-1.3.2.jar

https://772123.xyz/cdn/libbluray-j2se-1.3.2.jar

将jar文件移动到/usr/share/libbluray/,随后添加到系统路径。最后执行ffprobe即可消除警告。

echo 'export LIBBLURAY_CP=/usr/share/libbluray/libbluray-j2se-1.3.2.jar' >> ~/.bashrc
source ~/.bashrc
ShellScript

观察发现,ffprobe直接输出的json信息结构如下,并非Emby神医助手可以直接识别的格式。

{
    "programs": [
        {
            "program_id": 1,
            "program_num": 1,
            "nb_streams": 35,
            "pmt_pid": 256,
            "pcr_pid": 4097,
            "streams": [
                {
                    "index": 0,
                    "codec_name": "hevc",
                    "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)",
                    "profile": "Main 10",
                    "codec_type": "video",
                    "codec_tag_string": "HDMV",
                    "codec_tag": "0x564d4448",
                    "width": 3840,
                    "height": 2160,
                    "coded_width": 3840,
                    "coded_height": 2160,
                    "has_b_frames": 1,
                    "sample_aspect_ratio": "1:1",
                    "display_aspect_ratio": "16:9",
                    "pix_fmt": "yuv420p10le",
                    "level": 153,
                    "color_range": "tv",
                    "color_space": "bt2020nc",
                    "color_transfer": "smpte2084",
                    "color_primaries": "bt2020",
                    "chroma_location": "topleft",
                    "refs": 1,
                    "view_ids_available": "",
                    "view_pos_available": "",
                    "ts_id": "0",
                    "ts_packetsize": "192",
                    "id": "0x1011",
                    "r_frame_rate": "24000/1001",
                    "avg_frame_rate": "24000/1001",
                    "time_base": "1/90000",
                    "start_pts": 1048560,
                    "start_time": "11.650667",
                    "duration_ts": 1051616815,
                    "duration": "11684.631278",
                    "extradata_size": 726,
                    "disposition": {
                        "default": 0,
                        "dub": 0,
                        "original": 0,
                        "comment": 0,
                        "lyrics": 0,
                        "karaoke": 0,
                        "forced": 0,
                        "hearing_impaired": 0,
                        "visual_impaired": 0,
                        "clean_effects": 0,
                        "attached_pic": 0,
                        "timed_thumbnails": 0,
                        "non_diegetic": 0,
                        "captions": 0,
                        "descriptions": 0,
                        "metadata": 0,
                        "dependent": 0,
                        "still_image": 0,
                        "multilayer": 0
                    }
                },
                {
                    "index": 1,
                    "codec_name": "hevc",
                    "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)",
                    "profile": "Main 10",
                    "codec_type": "video",
                    "codec_tag_string": "HDMV",
                    "codec_tag": "0x564d4448",
                    "width": 1920
                    ......
JSON

而神医助手接受的JSON格式如下:

[{"MediaSourceInfo":{"Protocol":"File","Id":"5dfe5ad4-33dc-4d4d-8d31-de381f0ea0f9","Path":"/mnt/user/embydata/local/movie/云下载/[SGNB-296 V2][龙与地下城:侠盗荣耀 Dungeons & Dragons Honor Among Thieves 2023].iso","Type":"Default","Container":"MPEGTS","Size":93251432448,"Name":"[SGNB-296 V2][龙与地下城:侠盗荣耀 Dungeons & Dragons Honor Among Thieves 2023]","IsRemote":false,"HasMixedProtocols":false,"RunTimeTicks":80475067780,"SupportsTranscoding":true,"SupportsDirectStream":true,"SupportsDirectPlay":true,"IsInfiniteStream":false,"RequiresOpening":false,"RequiresClosing":false,"RequiresLooping":false,"SupportsProbing":false,"MediaStreams":[{"Codec":"hevc","Language":"und","TimeBase":"1/90000","DisplayTitle":"2160p HEVC","DisplayLanguage":"English","IsInterlaced":false,"IsDefault":false,"IsForced":false,"IsHearingImpaired":false,"Type":"Video","Index":0,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","PixelFormat":"yuv420p10le","Level":153,"BitRate":0,"RunTimeTicks":80474977780,"Profile":"Main 10","AspectRatio":"16:9","Width":3840,"Height":2160,"AverageFrameRate":23.976023976023978,"RealFrameRate":23.976023976023978,"BitDepth":0,"ChannelLayout":"","Channels":0,"SampleRate":0,"SubtitleLocationType":""},{"Codec":"hevc","Language":"und","TimeBase":"1/90000","DisplayTitle":"1080p HEVC","DisplayLanguage":"English","IsInterlaced":false,"IsDefault":false,"IsForced":false,"IsHearingImpaired":false,"Type":"Video","Index":1,"IsExternal":false,"IsTextSubtitleStream":false,"SupportsExternalStream":false,"Protocol":"File","PixelFormat":"yuv420p10le","Level":153,"BitRate":0,"RunTimeTicks":80474977780,"Profile":"Main 10","AspectRatio":"16:9","Width":1920,"Height":1080,"AverageFrameRate":23.976023976023978,"RealFrameRate":23.976023976023978,"BitDepth":0,"ChannelLayout":"","Channels":0,"SampleRate":0,"SubtitleLocationType":""},
....
JSON

那么我们的首要目的就是找出两者重合的部分,进行相应的格式转化,因为Unraid系统中缺失高级语言的运行环境,在AI帮助下,博主使用VPS部署了一个Nodejs api进行格式转化,代码如下:

// index.js
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json({ limit: '50mb' })); // 增加限制以处理大型 JSON
app.use(cors());

// Helper function for safe division
const safeDivision = (numerator, denominator) => {
    if (denominator === 0) return 0.0;
    return numerator / denominator;
};

// Helper function to parse frame rate strings like "24000/1001"
const parseFrameRate = (frameRateStr) => {
    const [numerator, denominator] = frameRateStr.split('/').map(Number);
    return safeDivision(numerator, denominator);
};

// Main conversion function
const convertFfprobeToCustomFormat = (ffprobeData) => {
    const formatInfo = ffprobeData.format || {};
    const streams = ffprobeData.streams || [];
    const programs = ffprobeData.programs || [];
    const chapters = ffprobeData.chapters || [];

    // Select all video streams
    const videoStreams = streams.filter(stream => stream.codec_type === 'video');
    if (videoStreams.length === 0) {
        throw new Error("没有找到视频流。");
    }

    // Select the longest video stream based on duration
    const mainVideoStream = videoStreams.reduce((prev, current) => {
        const prevDuration = parseFloat(prev.duration) || 0;
        const currentDuration = parseFloat(current.duration) || 0;
        return (currentDuration > prevDuration) ? current : prev;
    }, videoStreams[0]);

    // Generate unique ID
    const uniqueId = uuidv4();

    // Map streams to MediaStreams
    const mediaStreams = streams.map(stream => {
        // 检查 codec_type 是否存在
        if (!stream.codec_type) {
            console.warn(`警告:stream 中缺少 codec_type,跳过此流。流信息: ${JSON.stringify(stream)}`);
            return null; // 返回 null 表示跳过此流
        }

        const codecType = stream.codec_type.toLowerCase();
        const language = stream.tags && stream.tags.language ? stream.tags.language : "und"; // und = undefined

        // Calculate frame rates only for video streams
        let avgFrameRate = 0.0;
        let realFrameRate = 0.0;
        if (codecType === "video") {
            const avgFrameRateStr = stream.avg_frame_rate || "0/1";
            const rFrameRateStr = stream.r_frame_rate || "0/1";
            avgFrameRate = parseFrameRate(avgFrameRateStr);
            realFrameRate = parseFrameRate(rFrameRateStr);
        }

        return {
            "Codec": stream.codec_name || "",
            "Language": language,
            "TimeBase": stream.time_base || "",
            "DisplayTitle": "",
            "DisplayLanguage": "",
            "IsInterlaced": false, // 可根据需要进一步设置
            "IsDefault": Boolean(stream.disposition && stream.disposition.default),
            "IsForced": Boolean(stream.disposition && stream.disposition.forced),
            "IsHearingImpaired": Boolean(stream.disposition && stream.disposition.hearing_impaired),
            "Type": codecType.charAt(0).toUpperCase() + codecType.slice(1),
            "Index": stream.index !== undefined ? stream.index : -1,
            "IsExternal": false, // 根据实际情况调整
            "IsTextSubtitleStream": codecType === "subtitle",
            "SupportsExternalStream": false, // 根据实际情况调整
            "Protocol": "File",
            "PixelFormat": codecType === "video" ? (stream.pix_fmt || "") : "",
            "Level": codecType === "video" ? (stream.level || 0) : "",
            "BitRate": stream.bit_rate ? parseInt(stream.bit_rate) : 0,
            "RunTimeTicks": stream.duration ? Math.round(parseFloat(stream.duration) * 1e7) : 0, // 1 tick = 100纳秒
            "Profile": stream.profile || "",
            "AspectRatio": stream.display_aspect_ratio || "",
            "Width": stream.width || 0,
            "Height": stream.height || 0,
            "AverageFrameRate": avgFrameRate,
            "RealFrameRate": realFrameRate,
            "BitDepth": stream.bits_per_raw_sample ? parseInt(stream.bits_per_raw_sample) : 0,
            "ChannelLayout": codecType === "audio" ? (stream.channel_layout || "") : "",
            "Channels": codecType === "audio" ? (stream.channels || 0) : 0,
            "SampleRate": codecType === "audio" ? (stream.sample_rate ? parseInt(stream.sample_rate) : 0) : 0,
            "SubtitleLocationType": codecType === "subtitle" ? "InternalStream" : ""
            // 可以根据需要添加更多字段
        };
    }).filter(stream => stream !== null) // 过滤掉返回为 null 的流

    .map((stream, idx) => {
        // 设置 DisplayTitle 和 DisplayLanguage
        if (stream.Type === "Video") {
            stream.DisplayTitle = `${stream.Height}p ${stream.Codec.toUpperCase()}`;
            stream.DisplayLanguage = "English"; // 可以根据实际情况调整
        } else if (stream.Type === "Audio") {
            stream.DisplayTitle = stream.ChannelLayout || `${stream.Channels} Channels`;
            stream.DisplayLanguage = "English"; // 可以根据实际情况调整
        } else if (stream.Type === "Subtitle") {
            stream.DisplayTitle = `${stream.Codec.toUpperCase()} Subtitle`;
            stream.DisplayLanguage = "English"; // 可以根据实际情况调整
        }
        return stream;
    });

    // Map chapters
    const customChapters = chapters.map(chapter => {
        const startTime = parseFloat(chapter.start_time) || 0;
        return {
            "StartPositionTicks": Math.round(startTime * 1e7),
            "Name": (chapter.tags && chapter.tags.title) ? chapter.tags.title : `Chapter ${chapter.id || ""}`,
            "MarkerType": "Chapter",
            "ChapterIndex": chapter.id !== undefined ? chapter.id : 0
        };
    });

    // Extract file path
    let filePath = formatInfo.filename || "unknown";
    if (filePath.startsWith("bluray:")) {
        filePath = filePath.slice("bluray:".length);
    }

    // Construct MediaSourceInfo
    const mediaSourceInfo = {
        "MediaSourceInfo": {
            "Protocol": "File",
            "Id": uniqueId,
            "Path": filePath,
            "Type": "Default",
            "Container": formatInfo.format_name ? formatInfo.format_name.split(',')[0].toUpperCase() : "",
            "Size": formatInfo.size ? parseInt(formatInfo.size) : 0,
            "Name": filePath.split('/').pop().split('.')[0] || "Unknown",
            "IsRemote": false, // 根据实际情况调整
            "HasMixedProtocols": false, // 根据实际情况调整
            "RunTimeTicks": formatInfo.duration ? Math.round(parseFloat(formatInfo.duration) * 1e7) : 0,
            "SupportsTranscoding": true, // 根据实际需求调整
            "SupportsDirectStream": true, // 根据实际需求调整
            "SupportsDirectPlay": true, // 根据实际需求调整
            "IsInfiniteStream": false, // 根据实际需求调整
            "RequiresOpening": false, // 根据实际需求调整
            "RequiresClosing": false, // 根据实际需求调整
            "RequiresLooping": false, // 根据实际需求调整
            "SupportsProbing": false, // 根据实际需求调整
            "MediaStreams": mediaStreams,
            "Formats": [], // 根据需要填充
            "Bitrate": formatInfo.bit_rate ? parseInt(formatInfo.bit_rate) : 0,
            "RequiredHttpHeaders": {},
            "AddApiKeyToDirectStreamUrl": false, // 根据实际需求调整
            "ReadAtNativeFramerate": false, // 根据实际需求调整
            "ItemId": "" // 可根据需要生成或填充
        },
        "Chapters": customChapters
    };

    return mediaSourceInfo;
};

// Define the /format endpoint
app.post('/format', (req, res) => {
    const ffprobeData = req.body;

    if (!ffprobeData || typeof ffprobeData !== 'object') {
        return res.status(400).json({ error: "Invalid JSON payload." });
    }

    try {
        const customFormat = convertFfprobeToCustomFormat(ffprobeData);
        // Wrap the result in an array as per the example
        return res.json([customFormat]);
    } catch (error) {
        return res.status(500).json({ error: error.message });
    }
});

// Health check endpoint
app.get('/', (req, res) => {
    res.send("FFprobe Formatter API is running.");
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});
JavaScript

部署完毕后进行测试:

curl -X POST http://IP:3000/format \
     -H "Content-Type: application/json" \
     -d @/path/to/test-mediainfo.json
ShellScript

将返回的JSON文件放入Strm所在目录,进入Emby刷新对应影片元数据,此时就可以看到相应信息。使用Infuse播放也可以正常同步进度。注:ffprobe提取的mediainfo包含信息似乎并不全面,例如音频语言、字幕语言、码率、其他视频流等信息缺失,恳请了解ffmpeg的大佬给予指导。

注:神医助手最新版似乎无法自动提取Mediainfo,这里附上博主推荐的版本。https://772123.xyz/cdn/StrmAssistant.dll

一切准备就绪

自动化

最后的最后,就是要让Mediainfo的提取实现自动化,使用SHELL脚本实现,此脚本自动扫描Strm目录,寻找缺失mediainfo的影片,定位至媒体文件路径,使用ffprobe提取该媒体文件信息,将提取出的JSON发送给API进行格式转化,最后将其移动回Strm目录。

#!/bin/bash

# 设置脚本在遇到错误时退出
set -euo pipefail

# 定义目录路径
FFPROBE_DIR="/path/to/ffmpeg-master-latest-linux64-gpl/bin" #ffprobe所在路径
LINKS_DIR="/path/to/links" # strm所在路径
TEMP_DIR="/path/to/temp" # 用于格式转化的临时文件
MOVIES_DIR="/path/to/movie/" # 媒体文件所在目录
API_URL="http://YourIP:3000/format"  # 格式转化API地址,假设 API 端点为 /format

# 定义锁文件路径,使用 /tmp 目录避免权限问题
LOCK_FILE="/tmp/process_strm.lock"

# 函数:释放锁文件
cleanup() {
    rm -f "$LOCK_FILE"
    exit
}

# 检查是否已有实例在运行
if [ -e "$LOCK_FILE" ]; then
    echo "锁文件已存在,脚本可能正在运行。退出。"
    exit 1
fi

# 创建锁文件
touch "$LOCK_FILE"

# 确保在脚本退出时删除锁文件
trap cleanup EXIT

# 进入 ffprobe 所在目录
echo "进入目录: $FFPROBE_DIR"
cd "$FFPROBE_DIR" || { echo "无法进入目录 $FFPROBE_DIR"; exit 1; }

if [ ! -d "$TEMP_DIR" ]; then
    echo "创建 temp 目录: $TEMP_DIR"
    mkdir -p "$TEMP_DIR"
fi

# 遍历所有 .strm 文件
echo "扫描目录: $LINKS_DIR 以查找 .strm 文件"
find "$LINKS_DIR" -maxdepth 1 -type f -name "*.strm" | while read -r strm_file; do
    # 提取电影名(不带 .strm 扩展名)
    filename=$(basename "$strm_file" .strm)
    echo "处理电影: $filename"

    # 定义路径
    mediainfo_json_link="${LINKS_DIR}/${filename}-mediainfo.json"
    mediainfo_json_temp="${TEMP_DIR}/${filename}-mediainfo.json"
    iso_file="${MOVIES_DIR}/${filename}.iso"

    # 检查是否已经存在 mediainfo.json
    if [ -f "$mediainfo_json_link" ]; then
        echo "已存在 mediainfo.json 文件,跳过: $mediainfo_json_link"
        continue
    fi

    # 检查 ISO 文件是否存在
    if [ ! -f "$iso_file" ]; then
        echo "ISO 文件不存在,跳过: $iso_file"
        continue
    fi

    # 执行 ffprobe 命令,生成 mediainfo.json
    echo "运行 ffprobe 生成 mediainfo.json 文件"
    set +e  # 临时禁用错误退出,以处理可能的错误
    ./ffprobe -v error \
        -print_format json \
        -show_format \
        -show_streams \
        -show_chapters \
        -show_programs \
        bluray:"$iso_file" > "$mediainfo_json_temp"
    ffprobe_status=$?
    set -e  # 重新启用错误退出

    # 检查 ffprobe 是否成功以及文件是否创建
    if [ $ffprobe_status -ne 0 ] || [ ! -s "$mediainfo_json_temp" ]; then
        echo "ffprobe 执行失败或文件创建失败,跳过电影: $filename"
        continue
    fi

    # 调用 API 格式化 JSON
    echo "调用 API 进行格式化"
    response=$(curl -s -X POST "$API_URL" \
        -H "Content-Type: application/json" \
        -d @"$mediainfo_json_temp")

    # 检查 API 调用是否成功
    if [ $? -ne 0 ] || [ -z "$response" ]; then
        echo "API 调用失败或无响应,跳过电影: $filename"
        continue
    fi

    # 保存 API 返回的格式化 JSON 到 LINKS_DIR
    echo "保存格式化后的 JSON 到 $mediainfo_json_link"
    echo "$response" > "$mediainfo_json_link"

    # 等待 3 秒
    echo "等待 5  秒后处理下一个文件..."
    sleep 5

    echo "完成处理电影: $filename"
    echo "----------------------------------------"

done

echo "所有 .strm 文件处理完成。"
ShellScript

接下来定期运行上述脚本即可实现自动化提取,如果是Unraid系统,前往设置-User script添加此脚本,推荐设置一天一次的频率。

使用定时任务的方式,缺点是不能即时同步电影信息,如果和Auto_symlink一样,实时监控目标文件夹,发生变动则立即执行,效率会提高不少,当然这个功能需要Clouddrive会员。

#!/bin/bash
# clouddrive_monitor.sh
# 监控 /mnt/user/myCloudDrive 目录,一旦有文件变动,就执行 111.sh

WATCH_DIR="/mnt/user/myCloudDrive" # 该目录为媒体文件路径,注意不要写成Strm目录,如果监控Strm路径会陷入死循环。
ACTION_SCRIPT="/mnt/user/scripts/111.sh" #这里填写上一步脚本的路径

echo "[INFO] 开始监控目录: $WATCH_DIR"
echo "[INFO] 检测到变动时,会执行: $ACTION_SCRIPT"
echo

# 持续监听 -m、递归监听 -r,并监控 create/delete/modify/move 四种事件
inotifywait -m -r -e create,delete,modify,move "$WATCH_DIR" \
| while read path action file; do
    echo "[`date +'%Y-%m-%d %H:%M:%S'`] $action => $path$file"
    # 在这里执行脚本
    bash "$ACTION_SCRIPT"
done
ShellScript

如果部署了实时监控脚本,前一步的计划任务就可以删去,Unraid用户同样在设置-user script中添加次监控脚本,设置开机自动运行即可。最后进行测试,前往CLouddrive Web界面添加任意ISO格式电影,前往Emby,一切顺利的话稍后就刷新出新增电影,并且元数据一应俱全。

小结

infuse

使用Emby + strm方案替换原Alist后,infuse体验顿时丝滑无比,再也没有出现被115封禁的情况,如果Infuse没有显示媒体信息,可以大胆地删除元数据重新加载。但本方案仍然存在些许遗憾,ISO的媒体信息提取并不完善,对于TV剧集,目前也无法区分每一集的视频流,只能存储MKV格式的蓝光Remux。

如果各位发烧友有更好的方案,欢迎评论留言~

Strm+Emby=无痛刮削,应对网盘风控新策略最先出现在hiRipple

《黑悟空·神话》,现实如此魔幻

2024年8月30日 14:49

12.13 Update: TGA2024年度游戏《宇宙机器人》,大圣最终还是没有归来。

平心而论,今年的年度题目在博主眼中,大概是:黑悟空 > 暗喻幻想 > 黄金树幽影 > 宇宙机器人 > 小丑牌 > 最终幻想7 重生。游戏小年,任何一位上台都能挑出不少问题,TGA给机器人无非是索尼的影响力(三十周年),加上PS平台的情怀罢。

即便黑悟空更合适,但GOTY失之交臂并未让博主意外,祖国的3A处女作,问题同样不少,更别提某些恶臭的猴批群体了。索索巨献《宇宙机器人》,虽离年度还差些,但质量仍然到位,拉瑞安一个传话筒更没必要集火,大伙可以尽情骂TGA,在其他优秀作品底下拉屎起到的只有反效果。

时间来到8月20日,首部国产3A《黑悟空·神话》横空出世,成为了简中互联网的一颗核弹。一时间,无论是否玩过游戏,无论是否拥有设备,下至小学生,上至中年社畜,纷纷掏出键盘,加入到一场老中独有的战斗中。核弹的辐射尘之下,浮现出一幕幕幽默荒诞、令人忍俊不禁的喜剧,任何事物加入“爱国”这一标签后,仿佛性质就发生了变化,谁不能说它是电影中的《流浪地球》,数码界的华为,科幻界的《三体》?

正文前叠甲:

  • 博主并不认为《流浪地球》等国产作品自身素质存在严重不足,仅评论其引发的现象。博主于PC平台购入《黑悟空》并已经完整通关,对比生涯游玩过的其它作品,RGN给出9分评价。
  • 相信大部分国人与玩家都是理智的,大家能客观地谈论游戏的优缺点,博文仅仅对部分魔怔玩家吐槽,如有冒犯,那就冒犯了。

一个基于虚幻5引擎开发、宣传片极其出色的3A游戏,来自一个名不见经传的工作室,位于一个游戏工业甚至尚未起步的国家,很难相信它在玩家手里的实际表现。发售前仅提供媒体试玩、性能测试强制超采样、插帧等取巧的性能优化,已经隐约暗示了游戏的实际性能表现,特别是主机平台。

PS5平台的实际表示博主或多或少已经预料到,没有任何PS5版本的送测、没有任何的性能测试与试玩,国内可怜的主机占比就注定不会让开发商花大力气优化。老实说,如今去看主机平台的性能表现,甚至比预期稍好,算得上是三线工作室的优化水平。

但开发商对主机平台的态度令人寒心,不仅暗示后续不会继续优化,各种小问题也层出不穷。一个PS5游戏没有实体盘,没有丝毫DS手柄的适配,提供45帧平衡模式却不支持VRR(数毛社评价为“不平衡”的平衡模式),画面表现是本作宣传的重点,毕竟一个游戏的画质就像人的外表,做得越漂亮,第一印象就越好,作为营销手段可以有效地吸引圈外人,但到头来连HDR都没有适配。这些问题或许并不是多大的难题,连海外的独立工作室都能做得很好,譬如《动物井》《潜水员戴夫》,说难听点,手游《原神》《绝区零》都比其用心。既然希望占有海外市场,登陆主机平台,可否等质量达标之后再发售呢?(不考虑国人刷分的情况下,PS平台的MC玩家均分比PC低得多)

来看一看部分国人玩家与国内媒体的魔幻程度~

8月16日,媒体评分正式解禁。最先传来的评分是来自IGN中国的10分满分评价,顿时游戏直接登上老贼也望尘莫及的神坛。随后传来的是IGN本部和GS的8分评价,MC均分也止步于83分(后续降低到81分),这时大伙才知道先前的评分是个无足轻重的分部。

再随后是五花八门的国内媒体,清一色10分满分评价,以及各路UP、自媒体,纷纷吹其是游戏生涯的TOP1作品,ARPG的巅峰(实在不理解为什么口径如此统一,莫非是《美末2》后遗症?就连博主平时看的比较多的狗蛋也打出了未曾出现过的满分),此时的风向变成“外国人不懂西游记”,然后是“洋人不配评价”“中国人的游戏自己说了算”,发售前念想的“文化输出”、“洋人Reaction”变成了自嗨口号,“年度游戏”的颁奖者也由TGA变成了比比丽丽,此前人人嫌弃的硕士星空一举洗白,成为国内媒体攻入MC的启明星,想要世界人民的喜爱,何不多花思想做好本地化?

16日-20日这一段评分出炉、游戏未发售的时间,是互联网最精彩的时间。IGN的过往罪行被悉数搬出,政治正确、不懂游戏、歧视祖国的标签被逐渐焊死。“《元神》9分、《黄金树幽影》10分、《最后的生还者2》10分、《死亡搁浅》6分”等种种曾被认为不合理的评分挨个问斩,登陆Steam的开始刷差评,未登陆的那没办法,毕竟买主机成本过高。外网警察也整装待发,IGN评测者率先被问候全家、此后是油管与X的巡逻,一句差评就能体验网络的暴力。

一向被认为公正客观的GS也给出8分时,魔幻的网民在未玩过游戏的情况下,又想出了其它理由,认为是经过洋人对祖国的“打压”,刻意给出低分,并截几张MC占比极低的十八线媒体低分,殊不知《塞尔达传说》《博得之门3》等里程碑级别的作品里,此类媒体的数量也不少。既然大伙都说好,怎么可能是游戏本身的问题呢?

再来到8月20日,大部分玩家已经意识到了不对劲,理智的玩家们开始与魔怔人切割。大伙和自己的游戏经验一对比,发现评测中的缺点还真有这么回事,8-9分的评价不高不低,而PS5版没被送测确实事出有因,这表现大致也就MC80分水准。观看数毛社的技术评测后,才知道第一次做游戏的稚嫩,各种取巧的优化,图形技术的不成熟运用,与真正的世界大厂有着不小的差距。

魔怔群体仍未停止步伐,这时Steam高强度巡逻,差评即出警,以销量作为武器,斩杀一切异议之徒。事后分析出94%评价来自中国,他们给出的方案是,用自己蹩脚的机翻英文、日文伪装成洋大人,写好评然后退款。一面是评分只看国内媒体,国人的游戏洋人懂什么,一面是各种Reaction、某某国销量登顶、列书名“海外大主播直播玩”,这才叫“文化自信”!现在,男人们成为了“集帅”,创造了单机圈的饭圈,只不过追的明星是一款电子游戏。

抽象互联网

合理推测,中国人多但真正的玩家却很少,《黑悟空》销量如此火爆,很大程度上依靠了圈外蓝海玩家的购买力,他们此前可能不曾接触过游戏,或仅仅了解过手游,为了本作,他们甚至单独配了一台PC、跑到千米之外的网吧连打几通宵,第一次感受到单机游戏的魅力与虚幻5加持下的次世代画面,或许足以让他们认为,本作是一款世界顶级、完美无瑕的3A大作。

博主因此并未第一时间预购,约发售一周后,综合互联网各种信息,抛弃了最常游玩的PS5平台,在Steam购入了游戏。老实说,作为国内的第一款3A,哪一个玩家不希望它大获成功、甚至一举拿下TGA年度呢?即便它的质量并不够格。第一次的成功就可能让投资人们看到单机游戏的潜力,指不定就真成为中国游戏工业的开端。

平心而论,《黑悟空》作为游戏科学的处女作,表现相当不错的,但我们最欠缺的就是承认不足、虚心学习的勇气,整日的被害妄想、选择性无视缺点、出警与魔怔打分,只会让圈内的风气更臭,让中国游戏在世界上人人唾弃。很多国家的游戏业都是从无到有起步的过程,波兰CDPR《巫师》、捷克战马《天国拯救》、俄罗斯《原子之心》,这些工作室从默默无闻到世界认可,靠的不是国内这种状况。端正态度,实事求是,未来才可能出世真正的好作品。

通关啦

经历了约41小时,博主达成了第二结局,因为选择了Steam平台因此也没有后续的白金计划。下面专注于游戏本身,聊一聊优缺点。

先谈谈本作的各种毛病。性能表现部分,PC端问题同样不少,最严重的是光影渲染,室内与室外场景的过渡部分尤其显著,光与影的切换相当生硬。其次是全局光照,博主使用的显卡是7800XT,多个驱动下出现了全局光照预设高时,黑暗场景一片黑的情况,后续确认应该是BUG。其余性能问题包括粒子效果鬼畜、低分辨率纹理、过度锐化、面部光照、阴影闪烁、雪地/沙地无痕迹等,千奇百怪。不过很幸运的是,博主直到游戏通关也没有经历过任何闪退以及影响游戏进程的恶性BUG。

第二个大问题自然就是关卡设计,不光有逆天的空气墙,部分章节的地图也是又大又空,点名小西天-小雷音寺。博主玩这类游戏时,喜欢追求“探索感”,跑遍地图的每一处,寻找宝贝与隐藏,黑魂中亦是如此,但跑图在本作中却显得尤为折磨,经常会给出一个大平原以及大量远程小怪,篝火与篝火之间也没有明显的标志物,迷路导致的重复跑图配上主角缓慢的移动速度,未知之处究竟是空气墙还是隐藏关的思辨,实在是享受到了。小雷音寺的实景扫描,确实好看,但玩起来却是另一回事,偌大的寺庙与广场,堆满的梆硬武僧,玩起来犹如无头苍蝇,真要这么样照搬景点,不如抽时间去旅个游。对比之下,黑魂的地图就犹如精巧的机关盒,各个环节环环相扣,即便碰上同样空旷的法环,人家至少有马呀。

至于关卡的指引与隐藏,博主认为前五回问题不大,老贼游戏的支线同样需要仔细挖掘,但是第六回就尤其折磨,虽然开头的筋斗云着实过瘾,但在花果山来来回回飞了五六遍之后,剩下的只有烦躁。BOSS的指引实在算不上明确,放着闪电的鹿和战场上的犀牛算是容易,隐藏在云雾中的螳螂是不是有点过分。还有非常像拔大师剑的凤翅将军,想尽各种方法嗑药洗点提升生命,万万没想到是换一个法宝。集齐四件披挂之前,博主还前往了水帘洞试图拔金箍棒,前往大石敢当地图找隐藏(因为空气墙,遗漏了小西天的蕴石),令人失望的是,这些地方既没有封死、也没有任何提示,仅仅是摆出一个场景,困惑留给玩家。

不过既然是RGN 9分,那代表本作的含金量依旧很高。

动作游戏最重要的战斗系统,黑悟空打磨的还算不错,三种棍法各有其妙用,挺有创意,法术、法宝种类丰富。整个流程下来的各种BOSS体验都相当不错,虽说到不了ACT品类的顶尖水准,也没有只狼那般见招拆招的交互,但本作也有独特的一番风味。众BOSS的演出水准与压迫感都相当出色,大部分难度适中,打斗酣畅淋漓,最后的巨像演出甚至逼近了FF16的水准。

猴子只能拿棍棒一种武器是无可奈何,游科也尽可能地把棍子玩出花了,但不加入任何远程攻击手段与防御手段,还是显得玩法有些单一(为什么大圣残躯都会丢棍子),预输入与敌人AI也存在优化的空间。不过总的来说,这棍子还是打到爽了~

金箍棒

游戏的音画表现可以说也属于顶级水准,各种图形纹理BUG、没有HDR也架不住虚幻5的表现力,光影表现犹如现实。博主认为本作最强的人物与场景的碰撞效果,例如与植被、帘子的碰撞,它们的物理表现都相当自然,这是以往游戏前所未见的,另外就是体积云与各粒子特效,开头的天庭大战、筋斗云等效果出人意料。此外,中文配音十分到位,属于中配游戏的满分级别,各场景与头目的BGM都恰到好处(特别喜欢小黄龙那一曲)。

至于剧情,因为博主也不懂原著,不太能感受到所谓的雷点,体验下来居然觉得还不错,最喜欢的是影神图对各个小怪、头目的描述与介绍,满满的文化气息。要是老贼能学习一下,也不至于出现这么多魂学家。

PS:为了博主的人身安全,本文不会在博客外任何地方转载。

《黑悟空·神话》,现实如此魔幻最先出现在hiRipple

Ripple 的独立游戏 Top10

2024年5月11日 12:58

独立游戏像是电子游戏业界的一股清流,他们没有被商业的大手污染,承载的是开发者的爱与梦想。因此博主专为独立游戏品类建立了Top10专栏,以下具体的排名随时改变,并且仅代表个人口味。此外,无论是愿意接受安利还是有其他优秀作品推荐,都非常欢迎。

1、精灵与萤火意志。独立游戏数量众多,因此往往只有在某个方面特别突出,才能在其中鹤立鸡群,但因为开发成本、周期等限制,独立作品往往在玩法、剧情上更加突出。奥里系列却截然相反,将精力投入到视觉效果和背景音乐的制作中,最终得到的是一款不输音画3A、各方面几乎没有短板的神作。

https://www.douban.com/game/27067402/

2、蔚蓝。很难想象一款单纯的平台跳跃游戏在博主心中能有如此高的地位,甚至超过了游戏界的代表马里奥,博主也很难去说明自己为什么怎么青睐蔚蓝,可能是各种友好的玩法设定,也可能是循环无数的背景音乐,亦可能是简单又具有深度的剧情。所有要素都浑然一体,简而言之,玩过这款游戏后就爱上了它的一切。

https://www.douban.com/game/27598218/

3、Oneshot。一款画面简陋、流程短小的RPG游戏,此前博客安利了数遍,无需过多介绍。

https://www.douban.com/game/26938484/

4、Tunic,简称狐尔达。一款从封面就可以看出制作人多喜欢塞尔达的游戏,而且它的确做到了塞尔达的深度,属于是青出于蓝而胜于蓝了,这个世界总有无数个秘密等待挖掘,关卡设计登峰造极。

https://www.douban.com/game/26992456/

5、OPUS。非常惊艳的一款国产独立游戏(虽然是台湾工作室)。剧情水平相当之高,很难想象通关后居然让我落泪,实在是难以忘却。

https://www.douban.com/game/35493853/

6、意航员2。这是一款刚发售时甚至没有中文的游戏,也是一款和初代相隔十几年的游戏,与奥里相似,受微软器重才最终完成。它在我心中几乎是3D平台跳跃的巅峰,当然另一个巅峰自然是奥德赛,但意航员的风格与马里奥相差甚远,而博主个人也更加喜欢,因为不只是玩法,剧情表现也拥有相当可怕的深度。

https://www.douban.com/game/28458553/

7、茧。一款粗看让人提不起兴趣的游戏,但实际上手后,可以称之为关卡设计最令人舒适的游戏,谜题难度刚好卡在“让玩家认真思考而无需看攻略就能想出”的那种难度,各个部分的引导也非常出色,通关后仍然意犹未尽。

https://www.douban.com/game/35927414/

8、巨像之咆哮。其实也不知道这款游戏能不能称为独立游戏,但博主认为称其为商业游戏也相当不合适,它的气质过于独特,犹如一件艺术品。

https://www.douban.com/game/10755521/

9、以撒的结合。目前市面上有大量的独立游戏都采用了肉鸽的玩法,不仅可玩性强,而且成本也更低,但我个人不太愿意把这一类型捧得太高,当然除了可以被称为肉鸽祖先的以撒。

https://www.douban.com/game/26253018/

10、小小梦魇2。相比与前几个怪物,小小梦魇2的表现只能说中规中矩了,无论是在玩法、剧情、音乐还是画面上,但这款游戏却有这一种独特的风格,让我深陷其中,或许是不禁想起了千与千寻吧。

https://www.douban.com/game/34800645/

Ripple 的独立游戏 Top10最先出现在hiRipple

7500f+6750gre,记录人生中第一次装机

2024年4月29日 11:27

笔记本直升机一样的噪音以及稍显过时的性能,加上外接显示器各种奇怪bug,终于让我狠下心添置一台台式电脑。Update: 目前已更新为7800XT。

前言

虽然过两个月的618可以等来更优惠的价格,但最近实在闲得慌,决定直接动手开搞!其实最开始博主也是从考虑整机开始,一方面对于没经验的小白来说,装机确实不容易;另一方面买组装零件的价格基本不会比整机便宜多少。

但蹲了两三天pdd之后,整机总有令我不太满意的地方,首先就是遍地的“海景房”机箱,动辄100L的体积,实在欣赏不来这硕大的方块与全是光污染的风扇,而且大多数商家并不支持更换机箱。其次是廉价的配件,单看处理器和显卡,整机甚至能比博主现在组的机子低1k~1k5左右,但仔细一看主板内存等用料,都是能丐且丐,且不说后续能否战未来,连长时间使用稳定性都成问题。虽然这些整机支持升级部件,但算上升级的差价,不知不觉又不如自行组装了。

全家福

研究几天行情后,最终我的配置单如下:

  • 微星 Gaming Plus Wi-Fi + 7500f板u套装 1800
  • 散热 ak120 se 90
  • 机箱 铝小宝p90 六风扇 188
  • 显卡 撼讯 6750gre 12g
  • 电源 鑫谷 750w 显卡套装 2318
  • 内存 光威 ddr5 adie 32g 659
  • 硬盘 光威亦 1t 409 + 笔记本拆机 凯侠512g

固态

今年固态的涨价趋势也很离谱,买固态实属无奈之举,本想先买512g过渡,没想到512g固态的价格更是离谱,甚至快赶上1t的价格了。

特别是选用am5主板后,总希望能有pcie4满速的M2和adie的ddr5内存,最后内存加固态就达到了1k+,主板的散热马甲颜值还算不错。

主板自带马甲

实际测试下来,pcie4x4和之前的pcie3顺序读写速度差别很大,但不知道具体到游戏中有多少提升(4k随机读取只能说一模一样)。

内存

AM5只支持“高贵”的DDR5内存,价格近翻倍,性能却和ddr4相差无几。为了超频性能特意选择Adie颗粒,然而am5主板超频却超不了多少。

内存方面,博主就超了网上一个比较简单的小参方案,频率6400,fclk 2133具体的看图吧。

ddr5 adie 16g x 2
aida64跑分,感觉差不多
小参方案

主板

不得不吐槽微星这狗屎一样的debug灯,都24年的还会卡灯,首次装机点亮后cpu和ram灯闪个不停,总让人觉得哪个部件没装好,于是逐个拆内存、cpu排查,搞得焦头烂额,最后发现开机等一会就正常了。。

当初选用这块主板纯粹是因为白色面板+Wi-Fi6+前后typec+双m2接口,相比技嘉的gaming Wi-Fi强很多,7500f也没必要上小雕迫击炮这种旗舰了。但实际使用下来感觉却一般,装机后cpu超个定频定压直接开不了机,pbo开启后虽然可以开机,并且烤鸡也通过,但实际游戏中过十几分钟就会重启,内存超频收益也微乎其微,很难不怀疑是主板的问题,当然不超频的情况下大体还是稳定的。

主板

显卡

N卡还是A卡是一个比较纠结的地方,但目前N卡持续涨价,同性能A卡便宜1k左右,因此最终选择了A卡,后续如果有需求更换其实也很简单。目前A卡最强甜品卡应该就是6750gre 12g了,2k价位的性能差不多追平4060ti,还拥有12g的显存,高分辨率情况下优势巨大。

6750gre 12g

博主挑选这张卡主要看中了个人送保,方便出二手,但散热算是比较丐,不知道是显卡散热不行还是小机箱积热问题,超频到2800MHz必定掉驱动,最后选择官方的超频2666MHz倒是一切正常。

AMD驱动超频

CPU

不知道是散片CPU体质差还是主板供电拉垮,超频CPU会有各种各样的问题,包括无法开机、游戏重启等,因此最后选择AMD自动超频,仅仅加0.2GHZ,聊胜于无。

AIDA64 烤鸡

机箱

成品图

个人癖好是希望机箱尽可能小、紧凑,ARGB什么的并不在意,甚至最开始想组ITX,权衡考虑后认为性价比太低。 这款机箱是铝小宝p90,20L体积下达到了很好的兼容性,支持全长显卡、ATX电源、MATX主板、六个风扇位。至于散热、电源、主板等都往上加了一档,为未来升级留一点空间(装主板实在太麻烦了)。整机重量9kg左右。

另外,切忌买机箱配套的垃圾风扇,原以为自己对风扇要求不高,就顺便买了配套的六个白光风扇,然而使用后风扇各种噪音,无法调速、安装困难,老老实实上利民就完美了。

小机箱的理线非常之折磨,因此定制了一块灯板放在右侧,直接挡住电源和右边所有的线,看起来较为舒适,虽然这灯板质量也挺一般,但支持argb倒是意料之外。配合同款桌垫和键盘,很有塞批的感觉。

副屏

PC玩游戏还是需要折腾,因此实时检测fps很有必要,但大量参数直接放屏幕上又会破坏沉浸感。最后博主选择了一块小副屏,另购一条主板9pin转typec线,固定在机箱里,价格100左右,可以实现fps、cpu、gpu各种参数检测,cpu占用1%以下,挺不错。

Update:目前更新为7800XT二次元卡,以及玄武850K白色全模组电源,实现了纯白主机。

7500f+6750gre,记录人生中第一次装机最先出现在hiRipple

866数据结构与算法练习记录

2023年10月7日 21:22

既然决定跨考至HNU软件工程,这篇文章就用以记录刷过的数据结构算法题~

1.重构链表

void reverseLinklist(Linklist &L) {
    Lnode *p = NULL, *q = L, *temp;

    while (q != NULL) {
        temp = q->next;
        q->next = p;
        p = q;
        q = temp;
    }
    L = p; // 更新L的位置为反转后的链表的头部
}//头插法原地逆置

Lnode *findmiddle(Linklist L){
  Lnode *slow=L,*quick=L;
  while(quick!=NULL){
    slow=slow->next;
    quick=quick->next;
    if(quick!=NULL)quick=quick->next;
  }
  return slow;
}//快慢指针寻找中间节点


void reconstruct(Linklist &L){
  Lnode *mid=findmiddle(L);
  reverseLinklist(mid);
  Lnode *p=L,*q=mid,*temp1,*temp2;
  while(p!=mid&&q!=NULL){
    temp1=p->next;
    p->next=q;
    temp2=q->next;
    q->next=temp1;
    p=temp1;
    q=temp2;
  }
}//重构链表
C

2.求二叉树带权路径长度WPL

int WPL(Bitree T){
    if(T==NULL)return-1;
    InitQueue(Q);
    Bitnode *p=T;
    int level=0,wpl=0;
    EnQueue(Q,p);
    while(!isEmpty(Q)){
        int levelsize=QueueSize(Q);
        level++;
        for(int i=0;i<levelsize;i++){
            DeQueue(Q,p);
            if(p->lchild)EnQueue(Q,p->lchild);
            if(p->rchild)EnQueue(Q,p->rchild);
            if(isleaf(p))wpl=wpl+p->weight*level;
        }
    }
    return wpl;
}//求WPL

int WPL2(Bitree T,int length){
    if(T==NULL)return 0;
    if(isleaf(T)){
        return T->weight*length;
    }
    int leftwpl=WPL2(T->lchild,length+1);
    int rightwpl=WPL2(T->rchild,length+1);
    return leftwp + rightwpl;
}

int wplcaculator(Bitree T){
    return WPL2(T,0);
}//递归求WPL
C

3.计算结点最远距离

假定二叉树中两个结点的距离为两个结点之间边的条数(二叉树的边是无向边)。
编写一个函数求一棵二叉树中相距最远的两个结点之间的距离

struct TreeNode {
    struct TreeNode* left;
    struct TreeNode* right;
    int data;  // 此处为了完整性添加,实际算法中不会用到此data
};

int maxDistance = 0;

int depth(struct TreeNode* node) {
    if (node == NULL) {
        return 0;
    }

    // 递归计算左子树和右子树的深度
    int leftDepth = depth(node->left);
    int rightDepth = depth(node->right);

    // 更新最大距离
    if (leftDepth + rightDepth > maxDistance) {
        maxDistance = leftDepth + rightDepth;
    }

    // 返回当前节点的深度
    return 1 + (leftDepth > rightDepth ? leftDepth : rightDepth);
}

int findMaxDistance(struct TreeNode* root) {
    maxDistance = 0;
    depth(root);
    return maxDistance;
}
C

4.中序遍历的应用

void reform(Bitree T, int deep){
    if(T){
        if(isleaf(T)) {
        printf('%s',T->data);
        }else{
        if(deep>1)printf('(');
        reform(T->lchild,deep+1);
        printf('%s',T->data);
        reform(T->rchild,deep+1);
        if(deep>1)printf(')');
        }
    }else{
    return;
    }
}//二叉树转换
C

5.基于邻接多重表存储结构的图增加边(setEdge) 操作

// 边表节点
typedef struct ENode {
    int ivex, jvex; // 该边所依附的两个顶点的位置
    struct ENode *ilink, *jlink; // 分别指向依附于顶点ivex和jvex的下一条边
} ENode;

// 顶点表节点
typedef struct VNode {
    char data;  // 顶点信息
    ENode *firstedge;  // 指向第一条依附于该顶点的边
} VNode;

// 图
typedef struct {
    VNode adjmulist[MAX_VERTEX_NUM];  // 顶点数组
    int vexnum, edgenum;  // 图的当前顶点数和边数
} AMLGraph;

void setEdge(AMLGraph *G, int i, int j) {
    if (i == j) {
        printf("Self loops are not allowed.\n");
        return;
    }

    ENode *newEdge = (ENode *)malloc(sizeof(ENode));
    if (!newEdge) {
        printf("Memory allocation failed.\n");
        return;
    }

    // 设置边的信息
    newEdge->ivex = i;
    newEdge->jvex = j;

    // 将新边插入i的边表
    newEdge->ilink = G->adjmulist[i].firstedge;
    G->adjmulist[i].firstedge = newEdge;

    // 将新边插入j的边表
    newEdge->jlink = G->adjmulist[j].firstedge;
    G->adjmulist[j].firstedge = newEdge;

    G->edgenum++;  // 增加图的边数
}
C

6.基于ADT统计顶点出度与入度

void GraphDegree(Graph* G, int* indegree, int* outdegree{
  for(v=0;v<G->n();v++){
    for(w=G->First(v);w<G->n();w=G->Next(v,w){
      outdegree[v]++
      indegree[w]++
      }
    }
  }
C

7.Dijkstra算法的应用

// 使用Dijkstra算法计算从C市到所有其他城镇的最短距离
int* dijkstra(int graph[n][n], int src) {
    int dist[n]; // 存储从C市到每个城镇的最短距离
    bool visited[n]; // 标记城镇是否被访问
    for (int i = 0; i < n; i++) {
        dist[i] = INT_MAX;
        visited[i] = false;
    }
    dist[src] = 0;
    for (int count = 0; count < n - 1; count++) {
        int u = findMinimumDistance(dist, visited); // 寻找当前未访问的最短距离的城镇
        visited[u] = true;
        for (int v = 0; v < n; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }
    return dist;
}

int computeShortestDistanceSum(int graph[n][n], int src, int K) {
    int* dist = dijkstra(graph, src);
    sort(dist + 1, dist + n); // 排除C市,然后排序
    int sum = 0;
    for (int i = 1; i <= K; i++) {
        sum += dist[i];
    }
    return sum;
}
C
866数据结构与算法练习记录最先出现在hiRipple

从天马学生公寓搬到天马小区...

2023年7月9日 23:33

Update: 如今才发现,天马小区的全名是“长沙天马建筑工地附属小区”,如果有机会重选租房地点,那么首先被排除的应该就是这里。

突然发现好久没发新博文了,因为近几个月的事情实在是有点多。除了忙活期末、准备考研、通关王国之泪外,搬家也占了相当多的时间,那么就拿这篇文章水一水记录一下吧,不过此后的半年内应该很少再维护博客了,毕竟考研已经到了冲刺阶段。

前言

在参观了zzq的豪华校外租房后,实在难以抑制心中的冲动。更安静的环境、私人的小房间、大客厅大电视,校外租房的好处太多太多。个人而言,寝室的环境实在有些难以忍受,此前也考虑过租房的计划,无奈没找到合适的室友,多余的开销也比较大,于是一忍忍了三年。

在本学期期末,租房的想法愈发强烈,毕竟临近考研,在寝室学习是不可能的,每天图书馆又会在路上浪费大量时间,于是租房的这个方案似乎越来越完美,况且我的考研目标也是本校,顺利上岸的话研究生又可以住上三年,免去换寝室的烦恼。

从看房到合同,我们只花了两天的时间,随后的两天内即飞速将行李搬出,紧接后续的一周内,我们将新家逐一改造。老实说,租房的体验相当奇妙:白天在学校上课,晚上又似乎回到了家里,甚至有些不像是在上学的错觉...可以说,现在搬出来是相当后悔的,后悔没从大一就开始租房,导致走了不少弯路。

卧室

很幸运,次卧有一个相当大的书桌,以及明亮的窗户。

我的桌面
侧视图

新桌面上,包括常用的MacBook Air、贝尔金帝瓦雷elite(也被称为真力8351B,作为桌面音响的同时充当无线充电底座)、Switch底座(方便从客厅切换至卧室)、米家台灯1S、罗技Master3、紫米小风扇、小爱play增强版、KTC Miniled显示器、Kindle Paperwhite5、网易严选人体工学椅,以及王国之泪的特典桌垫。书桌最右侧则是常年吃灰的Win笔记本。

自从购入电视后,书桌上的显示器现在已经基本用不上,这也是租房前走的弯路之一,当初花了5k重金的屏幕,如今游戏、观影的效果竟然远不及电视,因此也只能充当Mac和Switch的拓展了。座椅下方则铺上了一层地毯,配合瑜伽垫在卧室健身环相当不错,书包以及各种杂物都能随意摆放~

米家APP

以小爱为中枢,我在卧室做了一套简易的智能家居。至于为什么没有选择Homeassistant,首先HA安装极其繁琐,再者我手头也缺少合适的硬件(N1盒子实在有点勉强...)。最重要的是,目前屋内所有的智能家居用米家APP+小爱(并非HomePod)即可控制,因此也没必要强行接入homekit,更何况租房不是自己的房子,点到为止即可。

大体来说包括:人来光线暗时自动开灯,人离开20分钟后自动熄灯;早晨自动拉开窗帘唤醒,晚上自动关闭窗帘;语音遥控空调、灭蚊器以及灯光设备等。

窗帘伴侣
人体传感器
夜灯&开关

在卧室房门出也设置了自动感应的夜灯,照亮挂画的同时也能防止夜晚看不清路。

Zelda挂画

客厅

客厅主要作为娱乐以及与好友吹水的场所,设备众多,快乐源泉。

客厅

客厅设备庞多,总共插满了6个插线板😂。其中影音设备包括:PS5*2(和舍友各一台)、Switch、Xbox Series S、Apple TV 第六代、雷鸟鹤6Pro电视、HDMI切换器、惠威M200音响,其他包括各种游戏收藏、高达模型、设定集、杂七杂八的米家智能家居。

米家
惠威
PS5
风扇
收藏

丐版家庭影院方案即:古董级惠威M200音响(说实话效果相当不错)、AppleTV、斐讯N1盒子。其中N1盒子作为AC2100的旁路由,一面可以负责科学上网(网飞、HBO、油管等需求),一面在本地搭建轻NAS服务。

我使用的局域网传输协议为WebDav,传统SMB协议的速度实属堪忧,对于蓝光原盘强迫症来说是几乎无法观看的,这也是舍弃闲置的win笔记本,反而购入N1盒子并刷入Openwrt作为NAS服务的主要原因。

Openwrt系统自带的许多特性就是专为折腾而生,跑个Docker也问题不大。我选择了Alist作为网络存储,原声支持Webdav协议,并采用多种储存方式:本地固态、阿里云、夸克(磁力)。不得不推荐的是夸克网盘,虽然需要会员才有完整的体验,但5R一年的会员可谓是不能再香,况且夸克是国内少有支持离线下载的网盘,配合神级网站BTNULL(电影)、Mikan(番剧)通过离线下载,最终实现蓝光原盘的在线观看。

在移动300M宽带的实际测试下,局域网+公网的传输带宽约为200Mbps~300Mbps(使用Infuse测试,受具体文件影响),高达上百GB的蓝光原盘也能流畅观影!

Mac端
TVOS
IOS端

家庭影院的客户端为Infuse,名副其实的IOS生态最强观影软件,订阅年费也只需72RMB,同时支持苹果生态(IOS、IPad、MacOS、TvOS全平台同步)、海报墙、自动刮削影片元数据、自动刷新共享库、字幕在线下载、格式包括HDR10+,杜比视界,杜比音效等。最重要的当然还有果味十足的精美UI,它也是我认为是目前市面上最漂亮的播放器UI,无聊时打开电视、翻翻海报墙也是一种享受。

流媒体:B站
航拍屏保

最后还要夸夸Apple Tv,购入之前对它并没有多高的期待,实际使用后我却认为这是苹果最具性价比的产品。仅仅600R的价格,能得到一个丝般顺滑的遥控器、国内外流媒体通吃的电视盒子,以及入门级的家庭影院终端。如果是IOS用户,还可以将iPhone作为遥控器无缝切换,打字、投屏或其他各种操作都得心应手。

FF16

家庭影院的核心--电视,这款雷鸟电视我也相当满意,仅仅3k出头就可以达到百级分区、1000nit峰值亮度、HDMI2.1接口(VRR、分区背光),观影和游戏都能实现真HDR效果,作为入门级再合适不过。

除此之外,雷鸟也支持HDMI一线通,配合无广告的开机,只需一键即可进入AppleTv,因此垃圾的原生系统感知几乎为零,综合体验相当纯净。如果是缺点的话,屏幕显示纯白会有轻微脏屏现象,运动补偿相当垃圾,HDMI2.1的接口数量也太少,只有一个完全不够用。

厨房

租房的另一个好处就是,终于可以自己做饭了~

厨房

厨房的设备采购花了较长的时间,虽然实际烧饭的次数也不多,但做做早餐,偶尔搞点下午茶、夜宵还是绰绰有余。设备包括简单的电饭煲、米家微波炉、米家空气炸锅等,最后以自己烧的一些小菜结束吧~

秘制酸菜鱼饺子
豚骨拉面
小酥肉
黄金奶香馒头
蛋挞
狮子头
番茄炒蛋
从天马学生公寓搬到天马小区...最先出现在hiRipple
❌
❌