作为一名电影爱好者,虽然在直播、在线视频点播等技术这么发达的今天仍然喜欢从网络下载电影资源观看。
有了群晖 NAS 后就一直尝试捣鼓各种下载套件,分别尝试过 Download Station、Aria2、Transmission、玩物下载等,都不尽如人意。群晖自带的 Download Station 还是比较鸡肋,BT 下载的 DIY 的空间虽然比较大,但是速度上始终不如迅雷,玩物下载的速度比较勉强,但在得知玩物下载在群晖后台搞小动作时也就果断卸载了,归根结底主要还是以前使用迅雷时已经习惯了迅雷的下载速度,尽管迅雷的广告多体验渣。
捣鼓了一圈后又回到了原点,于是尝试另一种曲线救国的路线,就是在群晖上安装 Windows 7 的虚拟机,然后在虚拟机中使用迅雷来下载,这样速度上能保证。不过新的问题是,在群晖中打开虚拟机然后再操作迅雷,操作流程比较繁琐。
那么有没有可能把在虚拟机中使用迅雷的下载流程自动化呢?既然都打算做自动化了,那么干脆做得更彻底些,将其他环节也尽量自动化,于是就有了本文。
规划的自动下载服务主要包含两个大的自动化功能:一个是自动启动迅雷去下载资源,这是核心的功能,另一个是自动搜刮可下载的电影资源,也就是说从人工找下载源再到将资源下载都做成自动化。

电影资源自动搜刮服务
以往人工去找电影下载源时都会先浏览好几个电影资源站点,看最近更新了什么资源,然后再去豆瓣查找电影相关的介绍和评分。当然这个过程反过来也是可以的,关键是按照豆瓣的评分信息找出符合喜好风格的可供下载的电影资源。这个过程可能几分钟就找好了,但也有花上 1 个小时也没找到满意的电影资源的情况,最后可能就不了了之。将这个过程自动化后可以省去不少找资源的时间。
搜刮服务说白了就是一个爬虫,具体实现细节不便多说,我这里主要使用了 Puppetter 来执行搜刮任务,然后将搜刮的结果存储到 MySQL 和 Redis。
之所以将电影的元信息和下载地址分开存储是考虑到电影元信息可以做永久存储,方便日后基于元信息库来构建家庭电影库,而下载地址只是临时存储,资源下载好后就删除了。
基于 Puppetter 的搜刮服务以及 MySQL 和 Redis 的存储都可以使用 Docker 来部署到群晖 NAS 中,另外我手头上还有树莓派,所以我把搜刮服务和存储服务都部署在了树莓派上,这样功耗可以更低。
下载地址方面,目前常用的下载协议有 BT、磁力、迅雷专用等,为了保证通用性可以都存储下来,其中迅雷专用的地址很多也是基于 BT、磁力的二次编码。
搜刮服务部署完后就可以使用 Crontab 来配置定时任务来执行。
电影资源自动下载服务
通过搜刮服务获取到下载源所需的信息后,接下来就轮到自动下载服务登场了。由于使用 Windows 7 来运行迅雷,那么自动下载服务就需要在两个不同的系统平台间执行任务。
宿主系统群晖 NAS 使用的是基于 Linux 的 DSM 系统,该系统本身有很多限制,好在可以安装 Docker 和虚拟机。
虚拟机使用的是群晖的 Virtual Machine 套件,在 VM 虚拟机上安装 Windows 的教程网上也有很多,这里不赘述, 但是在系统版本的选择上建议至少使用 Windows 7,这是群晖官方支持的最低版本,动手能力稍强一点的虽然也能安装 Windows XP,但是在 XP 上运行迅雷还是达不到理想的下载速度。

从上面的套娃架构图中可以看出,如果要执行自动化的任务,有两个关键的步骤: 1. 在群晖 DSM 上自动运行 VM 中的 Windows 7。 2. Windows 7 启动后自动运行迅雷来执行下载任务。
启动 Windows 7
要在群晖 DSM 中通过自动化命令的方式来启动 VM 中的 Windows 7 并不是一个简单的事,经过摸索尝试了三种方案。
方案一
一开始觉得 VM 方面应该有提供相应的开放 API,但是各种查阅发现并没有,而且发现群晖官网的提问区有人提过类似的问题没有回应,看来这条路走不通。
方案二
既然是基于 Linux 系统,通过 GUI 界面的操作最终都会映射到诸如文件修改,进程的启动等,VM 在运行时总会启动进程。于是通过 ssh 登录 DSM,然后使用 top 命令来查看启动的进程应该就能定位到具体的进程以及启动的参数等。果不其然找到了 VM 启动 Windows 7 的启动命令,发现 VM 是基于 Qemu 模拟器来实现的。
/usr/local/bin/qemu-system-x86_64 此处省略约2KB的参数拼接.../usr/local/bin/qemu-system-x86_64 此处省略约2KB的参数拼接.../usr/local/bin/qemu-system-x86_64 此处省略约2KB的参数拼接...
启动命令非常的长,各种参数拼接,长度接近 2KB,于是尝试复制该命令和参数来执行,发现命令启动时各种报错,Windows 7 死活无法启动,我对 Qemu 模拟器并不熟,临时抱佛脚学习 Qemu 的话按照启动参数的长度应该难度非常大,于是方案二也陷入了困局。
为了启动虚拟机去学习 Qemu 模拟器的原理和使用会让自动下载服务的搭建的计划直接搁浅,如果有熟悉这方面的大神可以尝试下。于是想到了不走寻常路的方案三。
方案三
群晖的 DSM 是通过 Web GUI 来操作的,对于 Linux 我是菜鸟,但是对于 Web 我非常熟悉,于是又搬出了 Puppetter,模拟人工点击操作来启动 Windows 7,一想到此都有点小骄傲感,JS 真没白学。使用 Puppetter 启动 Windows 7 的代码量在 100 行以内。
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ "args": [ "--no-sandbox", "--disable-setuid-sandbox", "--enable-logging", "--v=1" ] }); let page = await browser.newPage(); // 打开 Web 页面,进入到登录界面 await page.goto('群晖的Web访问地址'); // 输入账号密码进行登录 await page.type('#login_username', 'DSM用户名'); await page.type('#login_passwd', 'DSM密码'); await page.click('#login-btn'); // 检测 DSM 桌面是否有 VM 的图标 await page.waitForFunction(function () { const launchIcons = document.querySelectorAll('li.launch-icon'); if (launchIcons.length) { let launchIcon; for (let i = 0; i < launchIcons.length; i++) { launchIcon = launchIcons[i]; if (launchIcon.getAttribute('ext:qtip') === 'Virtual Machine Manager') { launchIcon.click(); return true; } } } }); // 启动 VM await page.waitForFunction(function () { const anchors = document.querySelectorAll('a.x-tree-node-anchor'); if (anchors.length) { let anchor; for (let i = 0; i < anchors.length; i++) { anchor = anchors[i]; if (anchor.textContent === 'Virtual Machine') { anchor.click(); return true; } } } }); // 启动 Windows 7 await page.waitForFunction(function () { const btns = document.querySelectorAll('td.x-toolbar-cell button.x-btn-text'); if (btns.length) { let btn; for (let i = 0; i < btns.length; i++) { btn = btns[i]; if (btns[i].getAttribute('aria-label') === 'Power on') { btn.click(); return true; } } } }); const result = await page.waitForFunction(function () { const status = document.querySelector('div.x-grid3-row-selected span.green-status'); return status && status.textContent === 'Running'; }); if (result) { console.log('windows 7 power on success'); } else { console.error('windows 7 power on failed'); } await browser.close(); })();const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ "args": [ "--no-sandbox", "--disable-setuid-sandbox", "--enable-logging", "--v=1" ] }); let page = await browser.newPage(); // 打开 Web 页面,进入到登录界面 await page.goto('群晖的Web访问地址'); // 输入账号密码进行登录 await page.type('#login_username', 'DSM用户名'); await page.type('#login_passwd', 'DSM密码'); await page.click('#login-btn'); // 检测 DSM 桌面是否有 VM 的图标 await page.waitForFunction(function () { const launchIcons = document.querySelectorAll('li.launch-icon'); if (launchIcons.length) { let launchIcon; for (let i = 0; i < launchIcons.length; i++) { launchIcon = launchIcons[i]; if (launchIcon.getAttribute('ext:qtip') === 'Virtual Machine Manager') { launchIcon.click(); return true; } } } }); // 启动 VM await page.waitForFunction(function () { const anchors = document.querySelectorAll('a.x-tree-node-anchor'); if (anchors.length) { let anchor; for (let i = 0; i < anchors.length; i++) { anchor = anchors[i]; if (anchor.textContent === 'Virtual Machine') { anchor.click(); return true; } } } }); // 启动 Windows 7 await page.waitForFunction(function () { const btns = document.querySelectorAll('td.x-toolbar-cell button.x-btn-text'); if (btns.length) { let btn; for (let i = 0; i < btns.length; i++) { btn = btns[i]; if (btns[i].getAttribute('aria-label') === 'Power on') { btn.click(); return true; } } } }); const result = await page.waitForFunction(function () { const status = document.querySelector('div.x-grid3-row-selected span.green-status'); return status && status.textContent === 'Running'; }); if (result) { console.log('windows 7 power on success'); } else { console.error('windows 7 power on failed'); } await browser.close(); })();const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ "args": [ "--no-sandbox", "--disable-setuid-sandbox", "--enable-logging", "--v=1" ] }); let page = await browser.newPage(); // 打开 Web 页面,进入到登录界面 await page.goto('群晖的Web访问地址'); // 输入账号密码进行登录 await page.type('#login_username', 'DSM用户名'); await page.type('#login_passwd', 'DSM密码'); await page.click('#login-btn'); // 检测 DSM 桌面是否有 VM 的图标 await page.waitForFunction(function () { const launchIcons = document.querySelectorAll('li.launch-icon'); if (launchIcons.length) { let launchIcon; for (let i = 0; i < launchIcons.length; i++) { launchIcon = launchIcons[i]; if (launchIcon.getAttribute('ext:qtip') === 'Virtual Machine Manager') { launchIcon.click(); return true; } } } }); // 启动 VM await page.waitForFunction(function () { const anchors = document.querySelectorAll('a.x-tree-node-anchor'); if (anchors.length) { let anchor; for (let i = 0; i < anchors.length; i++) { anchor = anchors[i]; if (anchor.textContent === 'Virtual Machine') { anchor.click(); return true; } } } }); // 启动 Windows 7 await page.waitForFunction(function () { const btns = document.querySelectorAll('td.x-toolbar-cell button.x-btn-text'); if (btns.length) { let btn; for (let i = 0; i < btns.length; i++) { btn = btns[i]; if (btns[i].getAttribute('aria-label') === 'Power on') { btn.click(); return true; } } } }); const result = await page.waitForFunction(function () { const status = document.querySelector('div.x-grid3-row-selected span.green-status'); return status && status.textContent === 'Running'; }); if (result) { console.log('windows 7 power on success'); } else { console.error('windows 7 power on failed'); } await browser.close(); })();
上面的代码不一定能适用各种上下文环境,需要看实际情况,比如 DSM 桌面有没有 VM 的图标,VM 中安装的虚拟机可能有多个等,使用 Puppetter 相当于按键精灵的方式,只要系统界面元素的位置有变动,就需要同步更新上面的脚本代码。方案三也是无奈之举,不过确实能满足当下的需求。
Windows 7 自动运行迅雷
进入了 Windows 7 的系统后,此时要实现的关键步骤就是自动运行迅雷来下载,首先想到的方案仍然是查阅迅雷是否有命令行的启动方式,查了一圈后发现我想多了,要是能通过命令行方式来启动,迅雷的广告卖给谁去,广告卖不出去怎么养活那么多员工。
既然无法通过开放 API 来启动迅雷,那么仍然可以使用按键精灵的方式来启动迅雷。但是光有启动迅雷的按键精灵还不够,还需要在 Windows 7 上执行一些包含逻辑的任务,这里我使用 Node.js 来执行这些任务,部分任务无法使用 Node.js 执行时则调用 DOS 命令来执行,关键是使用 Node.js 将这一系列任务组装起来。接下来不打算贴完整的代码了,来讲讲关键的步骤流程。
1. 映射网络磁盘
spawn('net', ['use', '\\\\192.168.1.10\\Movies', 'M:']);spawn('net', ['use', '\\\\192.168.1.10\\Movies', 'M:']);spawn('net', ['use', '\\\\192.168.1.10\\Movies', 'M:']);
Windows 7 是虚拟机,下载的资源肯定不能存储在虚拟机中,需要映射一个网络磁盘来作为下载盘。
2. 从数据存储中获取待下载的相关信息
3. 启动迅雷
spawn('D:\\xunlei\\ThunderStart.ext', [ '-StartType:DesktopIcon', '迅雷下载链接' ]);spawn('D:\\xunlei\\ThunderStart.ext', [ '-StartType:DesktopIcon', '迅雷下载链接' ]);spawn('D:\\xunlei\\ThunderStart.ext', [ '-StartType:DesktopIcon', '迅雷下载链接' ]);
使用 DOS 命令启动迅雷时可以跟一个迅雷下载链接的参数,此时迅雷启动时同时会建立下载任务,但是接下来的操作都需要人工操作,按键精灵会在此时接管剩下的操作流程。
4. 启动按键精灵
按键精灵有两种方案,一种是找现成的按键精灵软件,直接录制出相应的操作记录,一种是基于相关的按键基础库来自己编程实现,基础库方面有 AutoHotkey,而 Node.js 也有 RobotJS。原本我也想尝试使用 RobotJS,但是发现在 Windows 上要运行 RobotJS 安装一些基础的包特别麻烦,而且还非常占磁盘空间,于是就偷懒使用了第一种方案,也确实能满足需求。
5. 下载完成的整理工作
下载任务执行完毕后需要执行删除下载地址,整理下载的文件以及关机等任务。但是前面也提到迅雷并不会提供相应的开放 API 来检测任务执行完毕,于是想到的一种方案是监听迅雷下载时更新下载文件记录的方式来实现,使用 Node.js 的文件 watch API 就能进行监听的功能,迅雷对于下载中的文件更新规则如下:
update 爆炸新闻.1080p.mp4.xltd update 爆炸新闻.1080p.mp4.xltd.cfg remove 爆炸新闻.1080p.mp4.xltd remove 爆炸新闻.1080p.mp4.xltd.cfg update 爆炸新闻.1080p.mp4update 爆炸新闻.1080p.mp4.xltd update 爆炸新闻.1080p.mp4.xltd.cfg remove 爆炸新闻.1080p.mp4.xltd remove 爆炸新闻.1080p.mp4.xltd.cfg update 爆炸新闻.1080p.mp4update 爆炸新闻.1080p.mp4.xltd update 爆炸新闻.1080p.mp4.xltd.cfg remove 爆炸新闻.1080p.mp4.xltd remove 爆炸新闻.1080p.mp4.xltd.cfg update 爆炸新闻.1080p.mp4
通过观察上面的文件更新记录发现,当 .mp4
文件创建时就是下载结束的标志,此时文件已经更新完毕了,此时就可以放心执行下面的 Windows 的关机命令了。
exec('shutdown -s -t 30');exec('shutdown -s -t 30');exec('shutdown -s -t 30');
Windows 关机后,如果想让群晖的 DSM 也一起自动关机,可以使用检测 Windows 的 IP 是否仍存活的办法来判断是否关机。
至此使用群晖搭建自动下载服务的方案就全部介绍完毕了,服务搭建完后也确实节省了不少人工找资源下载的时间。虽然涉及到跨系统平台的任务执行,但是都能通过 Node.js 来把一系列的任务组装起来。
原文链接:https://blog.yiguochen.com/dsm-auto-download.html