Greasy Fork 支持 简体中文。

flash-game-downloader

一键下载 flash 游戏(swf),有限地支持(1)4399(2)7k7k(3)nitrome

目前為 2023-05-19 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name flash-game-downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.5
  5. // @description 一键下载 flash 游戏(swf),有限地支持(1)4399(2)7k7k(3)nitrome
  6. // @author 2690874578@qq.com
  7. // @match https://www.4399.com/flash/*
  8. // @match https://s2.4399.com
  9. // @match http://www.7k7k.com/swf/*.htm*
  10. // @match http://www.nitrome.com/games/*
  11. // @require https://cdn.staticfile.org/jszip/3.7.1/jszip.min.js
  12. // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE0XzIpIj4KPHBhdGggZD0iTTI4LjI2NjcgMEgzLjczMzMzQzEuNzA2NjcgMCAwIDEuNzA2NjcgMCAzLjczMzMzVjI4LjI2NjdDMCAzMC4yOTMzIDEuNzA2NjcgMzIgMy43MzMzMyAzMkgyOC4yNjY3QzMwLjI5MzMgMzIgMzIgMzAuMjkzMyAzMiAyOC4yNjY3VjMuNzMzMzNDMzIgMS43MDY2NyAzMC4yOTMzIDAgMjguMjY2NyAwWk0yMy40NjY3IDEwLjEzMzNDMjMuNDY2NyAxMC40NTMzIDIzLjI1MzMgMTAuNjY2NyAyMi45MzMzIDEwLjY2NjdDMjAuMjY2NyAxMC42NjY3IDIwLjE2IDEwLjk4NjcgMTkuNzMzMyAxMi4xNkMxOS42MjY3IDEyLjM3MzMgMTkuNjI2NyAxMi41ODY3IDE5LjUyIDEyLjhIMjEuODY2N0MyMi4xODY3IDEyLjggMjIuNCAxMy4wMTMzIDIyLjQgMTMuMzMzM1YxNy42QzIyLjQgMTcuOTIgMjIuMTg2NyAxOC4xMzMzIDIxLjg2NjcgMTguMTMzM0gxOC4wMjY3QzE2Ljg1MzMgMjIuMjkzMyAxMi40OCAyNi42NjY3IDggMjYuNjY2N0M3LjY4IDI2LjY2NjcgNy40NjY2NyAyNi40NTMzIDcuNDY2NjcgMjYuMTMzM1YyMS44NjY3QzcuNDY2NjcgMjEuNTQ2NyA3LjY4IDIxLjMzMzMgOCAyMS4zMzMzQzExLjMwNjcgMjEuMzMzMyAxMi4yNjY3IDE4LjY2NjcgMTMuMzMzMyAxNS4yNTMzQzEzLjU0NjcgMTQuNzIgMTMuNjUzMyAxNC4yOTMzIDEzLjg2NjcgMTMuNzZDMTUuMjUzMyA5LjkyIDE2Ljg1MzMgNS4zMzMzMyAyMi45MzMzIDUuMzMzMzNDMjMuMjUzMyA1LjMzMzMzIDIzLjQ2NjcgNS41NDY2NyAyMy40NjY3IDUuODY2NjdWMTAuMTMzM1oiIGZpbGw9IiNEODFFMDYiLz4KPHBhdGggZD0iTTIxLjE3ODkgMzYuMDg0MkMxOS45MTU4IDM2LjA4NDIgMTguNjUyNiAzNS41Nzg5IDE3LjY0MjEgMzQuNTY4NEMxNS42MjEgMzIuNTQ3NCAxNS42MjEgMjkuMzg5NSAxNy42NDIxIDI3LjM2ODRMMjAuMjk0NyAyNC43MTU4TDIyLjA2MzIgMjYuNDg0MkwxOS40MTA1IDI5LjEzNjhDMTguNCAzMC4xNDc0IDE4LjQgMzEuNjYzMiAxOS40MTA1IDMyLjY3MzdDMjAuNDIxIDMzLjY4NDIgMjEuOTM2OCAzMy42ODQyIDIyLjk0NzQgMzIuNjczN0wyNi40ODQyIDI5LjEzNjhDMjYuOTg5NSAyOC42MzE2IDI3LjI0MjEgMjggMjcuMjQyMSAyNy4zNjg0QzI3LjI0MjEgMjYuNzM2OCAyNi45ODk1IDI2LjEwNTMgMjYuNjEwNSAyNS42TDI1LjIyMTEgMjQuMzM2OEwyNi45ODk1IDIyLjU2ODRMMjguMzc4OSAyMy45NTc5QzI5LjI2MzIgMjQuODQyMSAyOS43Njg0IDI2LjEwNTMgMjkuNzY4NCAyNy40OTQ3QzI5Ljc2ODQgMjguODg0MiAyOS4yNjMyIDMwLjE0NzQgMjguMjUyNiAzMS4wMzE2TDI0LjcxNTggMzQuNTY4NEMyMy44MzE2IDM1LjU3ODkgMjIuNDQyMSAzNi4wODQyIDIxLjE3ODkgMzYuMDg0MlpNMjUuMjIxMSAyOS42NDIxTDIzLjgzMTYgMjguMzc4OUMyMS44MTA1IDI2LjM1NzkgMjEuODEwNSAyMy4yIDIzLjgzMTYgMjEuMTc4OUwyNy4zNjg0IDE3LjY0MjFDMjkuMzg5NSAxNS42MjEgMzIuNTQ3NCAxNS42MjEgMzQuNTY4NCAxNy42NDIxQzM2LjU4OTUgMTkuNjYzMiAzNi41ODk1IDIyLjgyMSAzNC41Njg0IDI0Ljg0MjFMMzEuOTE1OCAyNy40OTQ3TDMwLjE0NzQgMjUuNzI2M0wzMi44IDIzLjA3MzdDMzMuODEwNSAyMi4wNjMyIDMzLjgxMDUgMjAuNTQ3NCAzMi44IDE5LjUzNjhDMzEuNzg5NSAxOC41MjYzIDMwLjE0NzQgMTguNTI2MyAyOS4yNjMyIDE5LjUzNjhMMjUuNiAyMi45NDc0QzI0LjU4OTUgMjMuOTU3OSAyNC41ODk1IDI1LjQ3MzcgMjUuNiAyNi40ODQyTDI2Ljk4OTUgMjcuODczN0wyNS4yMjExIDI5LjY0MjFaIiBmaWxsPSIjMjcyNjM2Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTRfMiI+CjxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K
  13. // @grant none
  14. // @run-at document-idle
  15. // @license GPL-3.0-only
  16. // ==/UserScript==
  17.  
  18.  
  19. (function() {
  20. /**
  21. * 脚本级全局常量
  22. */
  23.  
  24. FLASH_ICON = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE0XzIpIj4KPHBhdGggZD0iTTI4LjI2NjcgMEgzLjczMzMzQzEuNzA2NjcgMCAwIDEuNzA2NjcgMCAzLjczMzMzVjI4LjI2NjdDMCAzMC4yOTMzIDEuNzA2NjcgMzIgMy43MzMzMyAzMkgyOC4yNjY3QzMwLjI5MzMgMzIgMzIgMzAuMjkzMyAzMiAyOC4yNjY3VjMuNzMzMzNDMzIgMS43MDY2NyAzMC4yOTMzIDAgMjguMjY2NyAwWk0yMy40NjY3IDEwLjEzMzNDMjMuNDY2NyAxMC40NTMzIDIzLjI1MzMgMTAuNjY2NyAyMi45MzMzIDEwLjY2NjdDMjAuMjY2NyAxMC42NjY3IDIwLjE2IDEwLjk4NjcgMTkuNzMzMyAxMi4xNkMxOS42MjY3IDEyLjM3MzMgMTkuNjI2NyAxMi41ODY3IDE5LjUyIDEyLjhIMjEuODY2N0MyMi4xODY3IDEyLjggMjIuNCAxMy4wMTMzIDIyLjQgMTMuMzMzM1YxNy42QzIyLjQgMTcuOTIgMjIuMTg2NyAxOC4xMzMzIDIxLjg2NjcgMTguMTMzM0gxOC4wMjY3QzE2Ljg1MzMgMjIuMjkzMyAxMi40OCAyNi42NjY3IDggMjYuNjY2N0M3LjY4IDI2LjY2NjcgNy40NjY2NyAyNi40NTMzIDcuNDY2NjcgMjYuMTMzM1YyMS44NjY3QzcuNDY2NjcgMjEuNTQ2NyA3LjY4IDIxLjMzMzMgOCAyMS4zMzMzQzExLjMwNjcgMjEuMzMzMyAxMi4yNjY3IDE4LjY2NjcgMTMuMzMzMyAxNS4yNTMzQzEzLjU0NjcgMTQuNzIgMTMuNjUzMyAxNC4yOTMzIDEzLjg2NjcgMTMuNzZDMTUuMjUzMyA5LjkyIDE2Ljg1MzMgNS4zMzMzMyAyMi45MzMzIDUuMzMzMzNDMjMuMjUzMyA1LjMzMzMzIDIzLjQ2NjcgNS41NDY2NyAyMy40NjY3IDUuODY2NjdWMTAuMTMzM1oiIGZpbGw9IiNEODFFMDYiLz4KPHBhdGggZD0iTTIxLjE3ODkgMzYuMDg0MkMxOS45MTU4IDM2LjA4NDIgMTguNjUyNiAzNS41Nzg5IDE3LjY0MjEgMzQuNTY4NEMxNS42MjEgMzIuNTQ3NCAxNS42MjEgMjkuMzg5NSAxNy42NDIxIDI3LjM2ODRMMjAuMjk0NyAyNC43MTU4TDIyLjA2MzIgMjYuNDg0MkwxOS40MTA1IDI5LjEzNjhDMTguNCAzMC4xNDc0IDE4LjQgMzEuNjYzMiAxOS40MTA1IDMyLjY3MzdDMjAuNDIxIDMzLjY4NDIgMjEuOTM2OCAzMy42ODQyIDIyLjk0NzQgMzIuNjczN0wyNi40ODQyIDI5LjEzNjhDMjYuOTg5NSAyOC42MzE2IDI3LjI0MjEgMjggMjcuMjQyMSAyNy4zNjg0QzI3LjI0MjEgMjYuNzM2OCAyNi45ODk1IDI2LjEwNTMgMjYuNjEwNSAyNS42TDI1LjIyMTEgMjQuMzM2OEwyNi45ODk1IDIyLjU2ODRMMjguMzc4OSAyMy45NTc5QzI5LjI2MzIgMjQuODQyMSAyOS43Njg0IDI2LjEwNTMgMjkuNzY4NCAyNy40OTQ3QzI5Ljc2ODQgMjguODg0MiAyOS4yNjMyIDMwLjE0NzQgMjguMjUyNiAzMS4wMzE2TDI0LjcxNTggMzQuNTY4NEMyMy44MzE2IDM1LjU3ODkgMjIuNDQyMSAzNi4wODQyIDIxLjE3ODkgMzYuMDg0MlpNMjUuMjIxMSAyOS42NDIxTDIzLjgzMTYgMjguMzc4OUMyMS44MTA1IDI2LjM1NzkgMjEuODEwNSAyMy4yIDIzLjgzMTYgMjEuMTc4OUwyNy4zNjg0IDE3LjY0MjFDMjkuMzg5NSAxNS42MjEgMzIuNTQ3NCAxNS42MjEgMzQuNTY4NCAxNy42NDIxQzM2LjU4OTUgMTkuNjYzMiAzNi41ODk1IDIyLjgyMSAzNC41Njg0IDI0Ljg0MjFMMzEuOTE1OCAyNy40OTQ3TDMwLjE0NzQgMjUuNzI2M0wzMi44IDIzLjA3MzdDMzMuODEwNSAyMi4wNjMyIDMzLjgxMDUgMjAuNTQ3NCAzMi44IDE5LjUzNjhDMzEuNzg5NSAxOC41MjYzIDMwLjE0NzQgMTguNTI2MyAyOS4yNjMyIDE5LjUzNjhMMjUuNiAyMi45NDc0QzI0LjU4OTUgMjMuOTU3OSAyNC41ODk1IDI1LjQ3MzcgMjUuNiAyNi40ODQyTDI2Ljk4OTUgMjcuODczN0wyNS4yMjExIDI5LjY0MjFaIiBmaWxsPSIjMjcyNjM2Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTRfMiI+CjxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K`;
  25.  
  26.  
  27. /**
  28. * 脚本级公用函数和对象
  29. */
  30.  
  31. /**
  32. * 元素选择器
  33. * @param {string} selector 选择器
  34. * @returns {Array<HTMLElement>} 元素列表
  35. */
  36. function $(selector) {
  37. const self = this?.querySelectorAll ? this : document;
  38. return [...self.querySelectorAll(selector)];
  39. }
  40.  
  41.  
  42. /**
  43. * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
  44. * @param {string} selector 选择器
  45. * @returns {Promise<Array<HTMLElement>>} 元素列表
  46. */
  47. async function $$(selector) {
  48. const self = this?.querySelectorAll ? this : document;
  49.  
  50. for (let i = 0; i < 10; i++) {
  51. let elems = [...self.querySelectorAll(selector)];
  52. if (elems.length > 0) {
  53. return elems;
  54. }
  55. await new Promise(r => setTimeout(r, 500));
  56. }
  57. throw Error(`"${selector}" not found`);
  58. }
  59.  
  60.  
  61. const util = {
  62. /**
  63. * 查找数组中某元素的全部位置,找不到返回空列表
  64. * @param {Array} arr
  65. * @param {Array} elem
  66. * @returns {Array<number>}
  67. */
  68. get_indexes: function(arr, elem) {
  69. const indexes = [];
  70. let from = 0;
  71. let i = arr.indexOf(elem, from);
  72.  
  73. while (i !== -1) {
  74. indexes.push(i);
  75. from = i + 1;
  76. i = arr.indexOf(elem, from);
  77. }
  78. return indexes;
  79. },
  80.  
  81. /**
  82. * 返回子数组位置,找不到返回-1
  83. * @param {Array<number>} arr 父数组
  84. * @param {Array<number>} sub_arr 子数组
  85. * @param {number} from 开始位置
  86. * @returns {number} index
  87. */
  88. index_of_sub_arr: function(arr, sub_arr, from) {
  89. // 如果子数组为空,则返回-1
  90. if (sub_arr.length === 0) return -1;
  91. // 初始化当前位置为from
  92. let position = from;
  93. // 算出最大循环次数
  94. const length = arr.length - sub_arr.length + 1;
  95.  
  96. // 循环查找子数组直到没有更多
  97. while (position < length) {
  98. // 如果当前位置的元素与子数组的第一个元素相等,则开始比较后续元素
  99. if (arr[position] === sub_arr[0]) {
  100. // 初始化匹配标志为真
  101. let match = true;
  102. // 循环比较后续元素,如果有不相等的,则将匹配标志设为假,并跳出循环
  103. for (let i = 1; i < sub_arr.length; i++) {
  104. if (arr[position + i] !== sub_arr[i]) {
  105. match = false;
  106. break;
  107. }
  108. }
  109. // 如果匹配标志为真,则说明找到了子数组,返回当前位置
  110. if (match) return position;
  111. }
  112. // 更新当前位置为下一个位置
  113. position++;
  114. }
  115. // 如果循环结束还没有找到子数组,则返回-1
  116. return -1;
  117. },
  118.  
  119. Socket: class Socket {
  120. /**
  121. * 创建套接字对象
  122. * @param {Window} target 目标窗口
  123. */
  124. constructor(target) {
  125. if (!(target.window && (target === target.window))) {
  126. console.log(target);
  127. throw new Error(`target is not a [Window Object]`);
  128. }
  129. this.target = target;
  130. this.connected = false;
  131. this.listeners = new Set();
  132. }
  133. get [Symbol.toStringTag]() { return "Socket"; }
  134. /**
  135. * 向目标窗口发消息
  136. * @param {*} message
  137. */
  138. talk(message) {
  139. if (!this.target) {
  140. throw new TypeError(
  141. `socket.target is not a window: ${this.target}`
  142. );
  143. }
  144. this.target.postMessage(message, "*");
  145. }
  146. /**
  147. * 添加捕获型监听器,返回实际添加的监听器
  148. * @param {Function} listener (e: MessageEvent) => {...}
  149. * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
  150. * @returns {Function} listener
  151. */
  152. listen(listener, once=false) {
  153. if (this.listeners.has(listener)) {
  154. return;
  155. }
  156. let real_listener = listener;
  157. // 包装监听器
  158. if (once) {
  159. const self = this;
  160. function wrapped(e) {
  161. listener(e);
  162. self.not_listen(wrapped);
  163. }
  164. real_listener = wrapped;
  165. }
  166. // 添加监听器
  167. this.listeners.add(real_listener);
  168. window.addEventListener(
  169. "message", real_listener, true
  170. );
  171. return real_listener;
  172. }
  173. /**
  174. * 移除socket上的捕获型监听器
  175. * @param {Function} listener (e: MessageEvent) => {...}
  176. */
  177. not_listen(listener) {
  178. console.log(listener);
  179. console.log(
  180. "listener delete operation:",
  181. this.listeners.delete(listener)
  182. );
  183. window.removeEventListener("message", listener, true);
  184. }
  185. /**
  186. * 检查对方来信是否为pong消息
  187. * @param {MessageEvent} e
  188. * @param {Function} resolve
  189. */
  190. _on_pong(e, resolve) {
  191. // 收到pong消息
  192. if (e.data.pong) {
  193. this.connected = true;
  194. this.listeners.forEach(
  195. listener => listener.ping ? this.not_listen(listener) : 0
  196. );
  197. console.log("Client: Connected!\n" + new Date());
  198. resolve(this);
  199. }
  200. }
  201. /**
  202. * 向对方发送ping消息
  203. * @returns {Promise<Socket>}
  204. */
  205. _ping() {
  206. return new Promise((resolve, reject) => {
  207. // 绑定pong检查监听器
  208. const listener = this.listen(
  209. e => this._on_pong(e, resolve)
  210. );
  211. listener.ping = true;
  212. // 5分钟后超时
  213. setTimeout(
  214. () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
  215. 5 * 60 * 1000
  216. );
  217. // 发送ping消息
  218. this.talk({ ping: true });
  219. });
  220. }
  221. /**
  222. * 检查对方来信是否为ping消息
  223. * @param {MessageEvent} e
  224. * @param {Function} resolve
  225. */
  226. _on_ping(e, resolve) {
  227. // 收到ping消息
  228. if (e.data.ping) {
  229. this.target = e.source;
  230. this.connected = true;
  231. this.listeners.forEach(
  232. listener => listener.pong ? this.not_listen(listener) : 0
  233. );
  234. console.log("Server: Connected!\n" + new Date());
  235. // resolve 后期约状态无法回退
  236. // 但后续代码仍可执行
  237. resolve(this);
  238. // 回应pong消息
  239. this.talk({ pong: true });
  240. }
  241. }
  242. /**
  243. * 当对方来信是为ping消息时回应pong消息
  244. * @returns {Promise<Socket>}
  245. */
  246. _pong() {
  247. return new Promise(resolve => {
  248. // 绑定ping检查监听器
  249. const listener = this.listen(
  250. e => this._on_ping(e, resolve)
  251. );
  252. listener.pong = true;
  253. });
  254. }
  255. /**
  256. * 连接至目标窗口
  257. * @param {boolean} talk_first 是否先发送ping消息
  258. * @param {Window} target 目标窗口
  259. * @returns {Promise<Socket>}
  260. */
  261. connect(talk_first) {
  262. // 先发起握手
  263. if (talk_first) {
  264. return this._ping();
  265. }
  266. // 后发起握手
  267. return this._pong();
  268. }
  269. },
  270.  
  271. /**
  272. * 以指定原因弹窗提示并抛出错误
  273. * @param {string} reason
  274. */
  275. raise: function(reason) {
  276. alert(reason);
  277. throw new Error(reason);
  278. },
  279. /**
  280. * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
  281. * @param {Iterable} iterable
  282. * @returns
  283. */
  284. enumerate: function* (iterable) {
  285. let i = 0;
  286. for (let value of iterable) {
  287. yield [i++, value];
  288. }
  289. },
  290. /**
  291. * 同步的迭代若干可迭代对象
  292. * @param {...Iterable} iterables
  293. * @returns
  294. */
  295. zip: function* (...iterables) {
  296. // 强制转为迭代器
  297. const iterators = iterables.map(
  298. iterable => iterable[Symbol.iterator]()
  299. );
  300. // 逐次迭代
  301. while (true) {
  302. let [done, values] = base.getAllValus(iterators);
  303. if (done) {
  304. return;
  305. }
  306. if (values.length === 1) {
  307. yield values[0];
  308. } else {
  309. yield values;
  310. }
  311. }
  312. },
  313. /**
  314. * 返回指定范围整数生成器
  315. * @param {number} end 如果只提供 end, 则返回 [0, end)
  316. * @param {number} end2 如果同时提供 end2, 则返回 [end, end2)
  317. * @param {number} step 步长, 可以为负数,不能为 0
  318. * @returns
  319. */
  320. range: function*(end, end2=null, step=1) {
  321. // 参数合法性校验
  322. if (step === 0) {
  323. throw new RangeError("step can't be zero");
  324. }
  325. const len = end2 - end;
  326. if (end2 && len && step && (len * step < 0)) {
  327. throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`);
  328. }
  329. // 生成范围
  330. end2 = end2 === null ? 0 : end2;
  331. let [small, big] = [end, end2].sort((a, b) => a - b);
  332. // 开始迭代
  333. if (step > 0) {
  334. for (let i = small; i < big; i += step) {
  335. yield i;
  336. }
  337. } else {
  338. for (let i = big; i > small; i += step) {
  339. yield i;
  340. }
  341. };
  342. },
  343. /**
  344. * 复制text到剪贴板
  345. * @param {string} text
  346. * @returns
  347. */
  348. copy_text: function(text) {
  349. // 输出到控制台和剪贴板
  350. console.log(
  351. text.length > 20 ?
  352. text.slice(0, 21) + "..." : text
  353. );
  354. if (!navigator.clipboard) {
  355. base.oldCopy(text);
  356. return;
  357. };
  358. navigator.clipboard
  359. .writeText(text)
  360. .catch(_ => base.oldCopy(text));
  361. },
  362. /**
  363. * 复制媒体到剪贴板
  364. * @param {Blob} blob
  365. */
  366. copy: async function(blob) {
  367. const data = [new ClipboardItem({ [blob.type]: blob })];
  368. try {
  369. await navigator.clipboard.write(data);
  370. console.log(`${blob.type} 成功复制到剪贴板`);
  371. } catch (err) {
  372. console.error(err.name, err.message);
  373. }
  374. },
  375. /**
  376. * 创建并下载文件
  377. * @param {string} file_name 文件名
  378. * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
  379. * @param {string} type 媒体类型,需要符合 MIME 标准
  380. */
  381. save: function(file_name, content, type="") {
  382. const blob = new Blob(
  383. [content], { type }
  384. );
  385. const size = (blob.size / 1024).toFixed(1);
  386. console.log(`blob saved, size: ${size} kb, type: ${blob.type}`);
  387. const url = URL.createObjectURL(blob);
  388. const a = document.createElement("a");
  389. a.download = file_name || "未命名文件";
  390. a.href = url;
  391. a.click();
  392. URL.revokeObjectURL(url);
  393. },
  394. sleep: async function(delay_ms) {
  395. return new Promise(
  396. resolve => setTimeout(resolve, delay_ms)
  397. );
  398. },
  399. /**
  400. * 取得get参数key对应的value
  401. * @param {string} key
  402. * @returns {string} value
  403. */
  404. get_param: function(key) {
  405. return new URL(location.href).searchParams.get(key);
  406. },
  407. /**
  408. * 等待直到函数返回true
  409. * @param {Function} is_ok 判断条件达成与否的函数
  410. * @param {number} timeout 最大等待秒数, 默认5000毫秒
  411. */
  412. wait_until: async function(is_ok, timeout=5000) {
  413. const gap = 200;
  414. let chances = parseInt(timeout / gap);
  415. chances = chances < 1 ? 1 : chances;
  416. while (! await is_ok()) {
  417. await this.sleep(200);
  418. chances -= 1;
  419. if (!chances) {
  420. break;
  421. }
  422. }
  423. },
  424. /**
  425. * 用try移除元素
  426. * @param {HTMLElement} element 要移除的元素
  427. */
  428. remove: function(element) {
  429. try {
  430. element.remove();
  431. } catch (e) {}
  432. },
  433. /**
  434. * 等待全部任务落定后返回值的列表
  435. * @param {Iterable<Promise>} tasks
  436. * @returns {Promise<Array>} values
  437. */
  438. gather: async function(tasks) {
  439. const results = await Promise.allSettled(tasks);
  440. return results
  441. .filter(result => result.value)
  442. .map(result => result.value);
  443. },
  444. /**
  445. * 使用xhr异步GET请求目标url,返回响应体blob
  446. * @param {string} url
  447. * @returns {Promise<Blob>} blob
  448. */
  449. xhr_get_blob: async function(url) {
  450. const xhr = new XMLHttpRequest();
  451. xhr.open("GET", url);
  452. xhr.responseType = "blob";
  453. return new Promise((resolve, reject) => {
  454. xhr.onload = () => {
  455. const code = xhr.status;
  456. if (code >= 200 && code <= 299) {
  457. resolve(xhr.response);
  458. }
  459. else {
  460. reject(new Error(`Network Error: ${code}`));
  461. }
  462. }
  463. xhr.send();
  464. });
  465. },
  466. /**
  467. * 加载CDN脚本
  468. * @param {string} url
  469. */
  470. load_web_script: async function(url) {
  471. try {
  472. // xhr+eval方式
  473. Function(
  474. await (await this.xhr_get_blob(url)).text()
  475. )();
  476. } catch(e) {
  477. console.error(e);
  478. // 嵌入<script>方式
  479. const script = document.createElement("script");
  480. script.src = url;
  481. document.body.append(script);
  482. }
  483. },
  484. };
  485.  
  486. /**
  487. * 域名级主函数
  488. */
  489.  
  490.  
  491. /**
  492. * 启动下载 4399 flash 游戏
  493. */
  494. function dl_flash_4399() {
  495. /**
  496. * 域名级全局常量、变量
  497. */
  498.  
  499. BASE_URL = "https://s2.4399.com/4399swf";
  500. let sock;
  501.  
  502.  
  503. async function send_url() {
  504. const title = $(".name a")[0].textContent.trim() || "flash游戏";
  505. const path = window._strGamePath;
  506.  
  507. if (!path) util.raise(
  508. "_strGamePath 不存在,找不到游戏文件路径"
  509. );
  510. if (!path.endsWith(".swf")) util.raise(
  511. `当前游戏不是 flash 游戏。\n游戏路径为:${path}`
  512. );
  513.  
  514. const id = "flash-dl-src";
  515. let iframe = $(`#${id}`)[0];
  516.  
  517. if (!iframe) {
  518. iframe = document.createElement("iframe");
  519. iframe.id = id;
  520. iframe.src = "https://s2.4399.com";
  521. document.body.append(iframe);
  522. sock = new util.Socket(iframe.contentWindow);
  523. await sock.connect(false);
  524. }
  525. sock.talk({
  526. flash_dl: true,
  527. url: BASE_URL + path,
  528. title,
  529. });
  530. }
  531.  
  532. function add_style() {
  533. const style = `
  534. <style>
  535. #flash-dl-btn {
  536. text-align: center;
  537. background: url("${FLASH_ICON}");
  538. background-repeat: no-repeat;
  539. background-position: top;
  540. width: 40px;
  541. padding-top: 30px;
  542. margin: 0 10px;
  543. float: left;
  544. display: inline;
  545. cursor: pointer;
  546. }
  547.  
  548. #flash-dl-src {
  549. display: none;
  550. }
  551. <style>
  552. `;
  553. document.head.insertAdjacentHTML(
  554. "beforeend", style
  555. );
  556. }
  557.  
  558. async function add_dl_btn() {
  559. const box = (await $$("#uplayer .fr"))[0];
  560.  
  561. // 修改误导性的下载按钮文本(下载4399游戏盒子)
  562. $("#down_a")[0].textContent = "盒子";
  563. // 新按钮
  564. const btn = document.createElement("a");
  565. btn.id = "flash-dl-btn";
  566. btn.textContent = "下载";
  567. btn.onfocus = () => btn.blur();
  568. btn.onclick = send_url;
  569. box.insertAdjacentElement("afterbegin", btn);
  570. }
  571.  
  572. (() => {
  573. console.log("enter: dl_flash");
  574. add_style();
  575. add_dl_btn();
  576. })();
  577. }
  578.  
  579. /**
  580. * 执行下载 4399 flash 游戏
  581. */
  582. function dl_flash_4399_in_origin() {
  583. /**
  584. * @param {MessageEvent} e
  585. */
  586. async function on_msg(e) {
  587. if (!e.data.flash_dl) return;
  588.  
  589. const { url, title } = e.data;
  590. const resp = await fetch(url, {
  591. headers: {
  592. "Host": "szhong.4399.com",
  593. "X-Requested-With": "ShockwaveFlash/34.0.0.282",
  594. }
  595. });
  596. if (!resp.ok) util.raise(
  597. `游戏下载失败,错误代码:${resp.status},原因:${resp.statusText}`
  598. );
  599.  
  600. const blob = await resp.blob();
  601. util.save(
  602. title.endsWith(".swf") ? title : title + ".swf",
  603. blob,
  604. "application/x-shockwave-flash"
  605. );
  606. }
  607.  
  608. (() => {
  609. console.log("enter: dl_flash_in_origin")
  610. if (window.top === window) return;
  611.  
  612. const sock = new util.Socket(window.top);
  613. sock.listen(on_msg);
  614. sock.connect(true);
  615. })();
  616. }
  617.  
  618. /**
  619. * 下载 7k7k flash 游戏
  620. */
  621. function dl_flash_7k7k() {
  622. /**
  623. * 域名级全局常量变量
  624. */
  625.  
  626. let swf_url;
  627. let dl_btn;
  628. const fnames = ["启动器.swf"];
  629. const HOW_TO_PLAY = `
  630. 【如何游玩多 SWF 文件组成的 Flash 游戏?】
  631. 1. 在你的电脑上下载并安装 python
  632. 2. python 解释器目录加入环境变量
  633. 3. 在解压为文件夹的游戏目录下打开 cmd powershell
  634. 4. 输入命令:python -m http.server --bind 0.0.0.0 5678
  635. 5. 回车执行上述命令
  636. 6. 用支持 Flash 的浏览器(如 [cef flash browser](https://github.com/Mzying2001/CefFlashBrowser) 访问:http://127.0.0.1:5678/启动器.swf
  637. `.replace(/ {2,}/g, "");
  638.  
  639.  
  640. /**
  641. * @returns {number}
  642. */
  643. function get_game_id() {
  644. return window?.gameInfo?.gameId ||
  645. parseInt(
  646. // http://www.7k7k.com/swf/28079.htm?abc
  647. location.pathname.match(/(?<=[/])[0-9]+?(?=[.]htm)/)[0]
  648. );
  649. }
  650.  
  651.  
  652. /**
  653. * @param {string | URL} url
  654. * @returns {Promise<ArrayBuffer>}
  655. */
  656. async function fetch_as_buffer(url) {
  657. const resp = await fetch(url);
  658. console.log(resp);
  659. if (!resp.ok) util.raise(`资源获取失败:${resp.status}`);
  660. return await resp.arrayBuffer();
  661. }
  662.  
  663.  
  664. /**
  665. * @param {string} fname
  666. */
  667. function update_url(fname) {
  668. const parts = swf_url.pathname.split("/");
  669. parts.splice(-1, 1, fname);
  670. swf_url.pathname = parts.join("/");
  671. }
  672.  
  673.  
  674. /**
  675. * @param {number} game_id
  676. * @returns {Promise<ArrayBuffer>}
  677. */
  678. async function get_swf(game_id) {
  679. // 查询游戏信息
  680. const info_url = `http://www.7k7k.com/swf/game/${game_id}/?time`;
  681. const resp = await fetch(info_url);
  682. console.log(resp);
  683. if (!resp.ok) util.raise(`游戏信息查询失败:${resp.status}`);
  684.  
  685. const info = await resp.json();
  686. console.log(info);
  687.  
  688. // 查询游戏页面 url
  689. const iframe_url = info?.result?.url;
  690. console.log(iframe_url);
  691. if (!iframe_url) util.raise(
  692. `找不到游戏页面路径:<游戏信息>.result.url 不存在`
  693. );
  694.  
  695. // 如果是游戏文件链接,直接下载,返回空结果用于终止后续函数
  696. if (iframe_url.endsWith(".swf")) {
  697. const swf = await fetch_as_buffer(iframe_url);
  698. const blob = new Blob(
  699. [swf], { type: "application/x-shockwave-flash" }
  700. );
  701. util.save(get_title() + ".swf", blob);
  702. return;
  703. }
  704.  
  705. // 从游戏页面 html 中提取游戏链接
  706. const resp2 = await fetch(iframe_url);
  707. console.log(resp2);
  708. if (!resp2.ok) util.raise(`游戏页面获取失败:${resp2.status}`);
  709.  
  710. const html = await resp2.text();
  711. const matches = html.match(/_src_\s*?=\s*?(['"])(.+)?\1/)
  712. || html.match(/var\s+?p\s*?=\s*(['"])(.+)?\1/);
  713. console.log(matches);
  714.  
  715. const swf_name = matches[2];
  716. console.log(swf_name);
  717.  
  718. if (!swf_name) {
  719. console.log(html);
  720. util.raise(`游戏路径查询失败:游戏页面中找不到 _src_ = "..."`);
  721. }
  722.  
  723. swf_url = new URL(iframe_url);
  724. update_url(swf_name);
  725.  
  726. // 下载游戏文件
  727. return await fetch_as_buffer(swf_url);
  728. }
  729.  
  730.  
  731. function get_title() {
  732. return document.title.split(",")[0];
  733. }
  734.  
  735.  
  736. /**
  737. * @param {ArrayBuffer} data
  738. * @returns {string}
  739. */
  740. function get_sub_fname(data) {
  741. const bytes = new Uint8Array(data);
  742. const end = util.index_of_sub_arr(
  743. // .swf
  744. bytes, [0x2e, 0x73, 0x77, 0x66], 0
  745. );
  746. if (end === -1) {
  747. console.log(`找不到子文件路径:找不到 .swf 字符串`);
  748. return "";
  749. }
  750.  
  751. const begin = bytes.lastIndexOf(0, end);
  752. if (begin === -1) {
  753. console.log(`找不到子文件路径:找不到 .swf 前的 \x00`);
  754. return "";
  755. }
  756.  
  757. return new TextDecoder()
  758. .decode(bytes.subarray(begin + 1, end)) + ".swf";
  759. }
  760.  
  761.  
  762. /**
  763. * @param {ArrayBuffer} swf
  764. * @param {Array<Blob>} files
  765. * @returns {Promise<void>}
  766. */
  767. async function collect_swfs(swf, files) {
  768. const fname = get_sub_fname(swf);
  769. if (!fname) return;
  770.  
  771. fnames.push(fname);
  772. update_url(fname);
  773.  
  774. const new_swf = await fetch_as_buffer(swf_url);
  775. files.push(new Blob(
  776. [new_swf], { type: "application/x-shockwave-flash" }
  777. ));
  778. collect_swfs(new_swf, files);
  779. }
  780.  
  781.  
  782. async function download_game() {
  783. const game_id = get_game_id();
  784. const swf = await get_swf(game_id);
  785. if (!swf) return;
  786.  
  787. const files = [new Blob(
  788. [swf], { type: "application/x-shockwave-flash" }
  789. )];
  790.  
  791. await collect_swfs(swf, files);
  792. const title = get_title();
  793. // 单文件游戏直接下载
  794. if (files.length === 1) {
  795. util.save(title + ".swf", files[0]);
  796. return;
  797. }
  798.  
  799. // 多文件游戏下载压缩包
  800. const zip = new window.JSZip();
  801. files.forEach((blob, i) => zip.file(
  802. fnames[i], blob, { binary: true }
  803. ));
  804. const help = new Blob([HOW_TO_PLAY]);
  805. zip.file("使用说明.txt", help, { binary: true });
  806.  
  807. // 导出
  808. const zip_blob = await zip.generateAsync({ type: "blob" });
  809. console.log(zip_blob);
  810. util.save(`${title}.zip`, zip_blob);
  811. }
  812.  
  813.  
  814. function add_style() {
  815. const style = `
  816. <style>
  817. #flash-dl-btn {
  818. background: url("${FLASH_ICON}");
  819. background-repeat: no-repeat;
  820. background-position: center;
  821. width: 40px;
  822. height: 100%;
  823. cursor: pointer;
  824. }
  825.  
  826. .play_header {
  827. display: flex !important;
  828. flex-direction: row;
  829. justify-content: space-between;
  830. }
  831.  
  832. .disabled {
  833. filter: grayscale(75%);
  834. pointer-events: none;
  835. }
  836. <style>
  837. `;
  838. document.head.insertAdjacentHTML(
  839. "beforeend", style
  840. );
  841. }
  842.  
  843.  
  844. async function add_btn() {
  845. dl_btn = document.createElement("button");
  846. dl_btn.id = "flash-dl-btn";
  847.  
  848. dl_btn.onclick = async () => {
  849. dl_btn.classList.add("disabled");
  850. try {
  851. await download_game();
  852. } catch (err) {
  853. console.error(err);
  854. alert(`下载失败,请在脚本主页反馈并附上网址,谢谢`);
  855. dl_btn.classList.remove("disabled");
  856. }
  857. dl_btn.classList.remove("disabled");
  858. };
  859.  
  860. const targets = await $$(".play_header");
  861. const target = targets[0];
  862. target.insertAdjacentElement("beforeend", dl_btn);
  863. }
  864.  
  865.  
  866. (() => {
  867. add_style();
  868. add_btn();
  869. })();
  870. }
  871.  
  872.  
  873. /**
  874. * 下载 nitrome flash 游戏
  875. */
  876. function dl_flash_nitrome() {
  877. function add_style() {
  878. const style = `
  879. <style>
  880. #flash-dl-btn {
  881. background: url("${FLASH_ICON}");
  882. background-repeat: no-repeat;
  883. background-position: center;
  884. width: 100%;
  885. height: 70px;
  886. cursor: pointer;
  887. display: flex;
  888. flex-direction: row;
  889. justify-content: space-around;
  890. }
  891.  
  892. .comment-info {
  893. flex-direction: column !important;
  894. }
  895. <style>
  896. `;
  897. document.head.insertAdjacentHTML(
  898. "beforeend", style
  899. );
  900. }
  901.  
  902.  
  903. function add_btn() {
  904. const dl_btn = document.createElement("a");
  905. // http://www.nitrome.com/games/finalninja/
  906. const fname = location.pathname.split("/").at(-2) + ".swf";
  907. dl_btn.download = fname;
  908. dl_btn.href = fname;
  909. dl_btn.target = "_blank";
  910. dl_btn.id = "flash-dl-btn";
  911. dl_btn.textContent = "下载游戏文件";
  912.  
  913. $(".comment-info")[0].insertAdjacentElement(
  914. "beforeend", dl_btn
  915. );
  916. }
  917.  
  918.  
  919. function main() {
  920. add_style();
  921. add_btn();
  922. }
  923.  
  924.  
  925. setTimeout(main, 3000);
  926. }
  927.  
  928.  
  929. /**
  930. * 路由函数,脚本主函数入口
  931. */
  932. function route() {
  933. console.log("enter: route");
  934.  
  935. const host = location.hostname;
  936. switch (host) {
  937. case "www.4399.com":
  938. dl_flash_4399();
  939. break;
  940.  
  941. case "s2.4399.com":
  942. dl_flash_4399_in_origin();
  943. break;
  944. case "www.7k7k.com":
  945. dl_flash_7k7k();
  946. break;
  947.  
  948. case "www.nitrome.com":
  949. dl_flash_nitrome();
  950. break;
  951.  
  952. default:
  953. console.log(`不受支持的域名:${host}`);
  954. break;
  955. }
  956. }
  957.  
  958.  
  959. setTimeout(route, 500);
  960.  
  961. /**
  962. * 更新日志
  963. * ---
  964. * 更新日期:2023/4/28
  965. * 更新版本:0.0.1
  966. * - 完成第一版 4399 flash 文件下载脚本
  967. * ---
  968. * 更新日期:2023/5/18
  969. * 更新版本:0.0.2
  970. * - 脚本名称变更
  971. * - 新增支持 7k7k
  972. * ---
  973. * 更新日期:2023/5/19
  974. * 更新版本:0.0.3
  975. * - 7k7k 游戏文件地址搜索增强
  976. * ---
  977. * 更新日期:2023/5/19
  978. * 更新版本:0.0.4
  979. * - 新增支持 nitrome
  980. * ---
  981. * 更新日期:2023/5/19
  982. * 更新版本:0.0.5
  983. * - 修复 7k7k 部分游戏下载失败的 bug
  984. */
  985. })();