flash-game-downloader

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

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