MoreRSS

site iconRipple修改

来自浙江省,湖南大学研究生。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Ripple的 RSS 预览

hiRipple 2024年度总结

2025-01-06 17:02:11

一转眼一年又过去了。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全年评分

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

2024-12-29 15:27:49

前言

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竞争,缺失了一键内网穿透、音乐库等功能,而对比开源的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

而Emby接受的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的提取实现自动化,使用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-08-30 14:49:35

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

M2 Pro MacBook Pro 16+512 乞丐版开箱

2024-07-17 15:38:19

成为研究生之后,博主终于下定决心升级一下Mac了,战斗了接近三年的MacBook Air M1也终于可以宣布退役,新加入的M2 Pro MacBook Pro将成为主力机。

开箱之前谈一谈升级的一些原因:

  • 一块更细腻、更漂亮的Miniled刘海屏。从M2 Air开始,博主就眼馋这块刘海已久,兼具美感和功能性,老屏幕的边框实在有些厚。
  • 更大的内存。M1 Air是乞丐版的8G内存,在日常使用中已经遇到不少次内存不足了,升级到16G很有必要。
  • 接口与音响。MagSafe充电接口终于回归,充电时不仅不占用C口,而且Pro本身也比Air多一个C口,充电、传输数据、外界屏幕等全功能支持,再也不用担心接口不够用了。Pro的六扬声器系统提升也很大,即便家里有桌面音响,但MacBook本身就需要便携属性。

总体来说,新Mac除了续航保持不变外,几乎全方面都进行了升级。除开以上三点,Pro的键盘键程也更长、触控板面积更大、WI-FI与蓝牙也进行了升级等,但重量与厚度的参数也提高了,这可不是什么好事。

下面直接进行开箱,博主选择的是全新的官翻机,主打性价比。

开箱!

打开官方的牛皮纸包装,内侧就是电脑本体了。官翻的盒子与常规款不用,正面只有简洁的“MacBook Pro”。

A面

从底部撕下Apple熟悉的两条封条,正面打开包装,即可露出被薄膜包裹的MacBookA面。

取出机子并打开,虽说是官翻,但成色和常规新机无异,并未发现任何使用痕迹。

Mac没有iPhone那么环保,包装盒里还附赠了67W充电器和编织线,编织的手感非常不错。

顶级刘海屏

充电后打开机子的第一印象就是这漂亮的刘海屏了,不仅仅是刘海部分,屏幕自身的参数也非常夸张,峰值亮度、刷新率、分辨率、色彩都与Air是天壤之别。作为miniled屏幕,可以直接显示HDR内容并且不用担心OLED烧屏的毛病,屏幕的分区足够多,加上Apple的调教技术,几乎看不到光晕。

官方参数对比
对比

显示效果比当初5k购入的KTC miniled提升很大(图中屏幕角度不同,并且存在反光,只能表现大致的水平)。

官翻的Mac保修时间只有一年,7月17日激活,保修至25年7月16日,满保一年,说明机子并非后封。电池循环4次,稍微有些多。

拿到机器的第二个感觉就是比老Air厚、重很多,厚度几乎是两倍,重量增加一斤,相比之下缺少了Air的精致感,感觉像着一坨大铁块。

C面

从C面看,因为老Air的楔形设计,新Pro的厚度几乎达到了原先的三四倍,相当夸张。即便Air盒盖之后,厚度仍然比Pro低不少。

合盖之后从A面看,前端部分Pro的厚度已经翻倍,到后端时接近1.5倍左右。

综上,简单体验新Pro之后,大致是比较满意的,但厚度与重量带来的弊端有些超出预期,毕竟在博主眼里,MacBook最大的竞争力就是便携性,离电的高性能与强续航、轻薄美观的机身都是升级Mac的最大动力,但屏幕与性能提高就需要厚度与重量的妥协,这不禁让博主更期待未来Air系列的模样。

M2 Pro MacBook Pro 16+512 乞丐版开箱最先出现在hiRipple

13Days 旅行日记:日本关西&amp;关东-韩国首尔-中国福州

2024-07-15 16:47:48

四年的大学时光转瞬即逝,落幕之际的一场旅行或许终生难忘。

本篇博文将以流水账的形式,记录博主毕业旅行的点点滴滴,亦或者为爱好相似的各位提供一些参考(博主行程的全部文档攻略置于本文末尾)。

日本作为本次行程的重中之重,耗费了我们最多的时间,选择它作为目的地,不外乎几种原因:

  • 旅行难度低,对游客友好,适合第一次出国
  • 日本文化对于我们,具备极强的吸引力
  • 地理位置离祖国很近,汇率低物价适中

五个人因为主机游戏相识,因此本次行程也会将大量的时间安排在与IP相关的打卡、购物上,自然风光的占比较小。总计十天的日程中,分5日关西大阪,5日关东东京,两个城市分别订了5日的酒店,以新干线作为交通方式。简要行程如下:DAY1 大阪市区,DAY2 大阪环球影城,DAY3 奈良,DAY4 京都,DAY5 大阪梅田&心斋桥购物,DAY6 东京晴空塔&秋叶原,DAY7 涩谷&秋叶原,DAY8 东京迪士尼乐园,DAY9 富士山一日游,DAY 10 三鹰美术馆&东京银座。

出发前大家共同制作了详细的旅行计划,行程中的大部分安排也成功按计划进行,而且非常幸运地几乎没有碰到雨天。但实际出行后才后知后觉,计划赶不上变化,购物花费的时间远比计划更多,十日旅行带来的疲惫感也比预期强烈,最后不得不砍掉一些项目。

DAY 1 落地&大阪市区

第一次出境比想象中顺利,基本是使用入境申报表与电子QR码一路向前,第一次听见日式英语确实会愣老半天😂。下飞机后就地铁直奔大阪酒店,一晚人均200+,但体验出乎意料地不错,全程自助入住,房间内很干净,智能马桶、冰箱、微波炉等各种设施都一应俱全,日本的浴缸泡着真舒服~

我们的酒店靠近通天阁,距离日本桥、心斋桥也只需要步行10-20分钟,交通那是相当方便。

日本第一顿饭选择了怪猎酒馆,也是队伍里怪猎老粉眼馋好久的一家餐厅。初到日本碰到了不少趣事,连进个餐厅大伙都面面相觑,不敢上前和服务员交谈(当然后面脸皮就越来越厚了)。但总体来说,这家餐厅比较一般,如图一份套餐 + 一瓶饮料就消费了122人民币,味道也平淡无奇,更推荐各位老猎人们选择环球内的怪猎餐厅。比较有意思的是店内的怪猎挑战活动,10分钟内刷完两只泡狐龙即可优惠500日元。

随后是心斋桥和道顿堀的闲逛时间,品尝下当地的便利店和小吃。

第一次逛日本商场,感觉一切都很新鲜,不知是否是电商的锅,总觉得日本的线下商店比国内繁华地多,商品摆设也更赏心悦目,给人一种更愿意逛的欲望。

成功淘到FF16 吉尔-希瓦 金属画一幅~


DAY 2 大阪环球影城

为避免暑期大学生大军,大阪第二天我们就立马决定前往环球影城。

博主一行人的方案是,进园区后直奔哈利波特园区,然后手机APP抽取马里奥园区的整理券,我们抽到了11:40左右的场次。

园区内的禁忌之旅体验很不错,形式类似于3D室内过山车,兼并观赏性与刺激感,美中不足的是部分画面比较糊,操着一口日语的哈利也有点幽默。另一个项目鹰马飞行因为排队时间过长,并且看起来就像是普通过山车换了一个头,直接无情舍弃。

黄油啤酒

劝退环球的黄油啤酒,为了照顾小朋友们,黄油啤酒不含任何酒精,而且喝起来实在是太甜了,犹如拿了一杯糖浆在手上。等待马里奥园区入场的这段时间内,就随意在霍格莫德逛逛,买魔杖、喝啤酒、看表演,推荐购买非互动款魔杖,虽然只有哈利与邓布利多的,但质感厚实很多。

接下来就是心心念念的马里奥园区,原计划进入园区后就直奔蘑菇餐厅,到了才发现餐厅需要提前一日预约,实属遗憾。马里奥园区的布景还是相当精致的,观赏性极佳,大家都戴着马里奥/路易基的帽子,仿佛真的进入了任天堂的世界,有些后悔没有买/租一个手环。

园区最主要的两个项目是 酷霸王的挑战书和耀西小火车,前者是4D卡丁车,在车上戴着3D眼镜,射击敌人获得高分,后者是坐上小火车围绕园区走一圈。作为一个任豚,说实话老任这边的项目稍显敷衍,可以说看的价值远大于玩。

出园区后,我们前往了怪猎餐厅,即便处于游乐园内,这家餐厅的性价比依旧比之前的怪猎酒馆高,一份套餐可以吃得很饱,还是比较推荐的。

夜晚的安排就比较随意,怪猎XR项目同样因为没有预约成为了遗憾,不过晚上的环球人流少了一大半,大部分项目都已不用排队,大伙们都玩的尽兴。

大阪环球这一天给我的感受相当奇特,是一种“游乐园就该这样啊~”的感觉。不禁感叹日本服务业和国内完全是天壤之别,被日本人的热情程度深深震撼。飞天翼龙上升时,轨道下的游客会和玩家们挥手打招呼;穿一件宝可梦的T恤,工作人员会惊讶地聊聊宝可梦,戴着库巴的帽子游玩项目,也会被夸赞:Bowser,卡酷伊!

13Days 旅行日记:日本关西&关东-韩国首尔-中国福州最先出现在hiRipple

Ripple 的独立游戏 Top10

2024-05-11 12:58:34

独立游戏像是电子游戏业界的一股清流,他们没有被商业的大手污染,承载的是开发者的爱与梦想。因此博主专为独立游戏品类建立了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