動畫瘋下載器

取得動畫的 m3u8 網址,並可使用 PotPlayer 播放

  1. // ==UserScript==
  2. // @name 動畫瘋下載器
  3. // @namespace
  4. // @description 取得動畫的 m3u8 網址,並可使用 PotPlayer 播放
  5. // @version 1.6.5
  6. // @author
  7. // @match https://ani.gamer.com.tw/animeVideo.php?sn=*
  8. // @connect ani.gamer.com.tw
  9. // @grant none
  10. // @namespace
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15. //保存路徑
  16. const path = '%USERPROFILE%/Downloads'
  17. //複製模式(0:複製完整指令|1:複製URL+名稱)
  18. const mode = 0;
  19.  
  20. //注入樣式到頁面中
  21. function injectStyles(css) {
  22. const style = document.createElement('style'); // 創建 <style> 元素
  23. style.textContent = css; // 設置樣式內容
  24. document.head.appendChild(style); // 將 <style> 添加到 <head>
  25. }
  26.  
  27. //解析 m3u8 播放列表,並在頁面上生成按鈕供使用者複製鏈接或使用 PotPlayer 播放
  28. async function parsePlaylist() {
  29. const req = playlist.src; // 獲取播放列表的 URL
  30. const response = await fetch(req); // 請求播放列表
  31. const text = await response.text(); // 獲取回應的文字內容
  32. const urlPrefix = req.replace(/playlist.+/, ''); // 提取 URL 前綴
  33. const m3u8List = text.match(/=\d+x\d+\n.+/g); // 匹配所有清晰度的 m3u8 連結
  34.  
  35. // 生成動畫名稱,作為文件名使用
  36. const Name = document.title.replace(" 線上看 - 巴哈姆特動畫瘋", "").replace(/[\/:*?"<>|]/g, '_');
  37. titleDisplay.textContent = '複製下載指令或使用外部播放器';
  38.  
  39. let m3u8_url2
  40. // 遍歷每個 m3u8 連結,生成對應的按鈕
  41. for (const item of m3u8List) {
  42. let key = item.match(/=\d+x(\d+)/)[1]; // 提取清晰度(如 720)
  43. let m3u8_url = item.match(/.*chunklist.+/)[0]; // 提取 m3u8 文件的相對路徑
  44. m3u8_url = urlPrefix + m3u8_url; // 拼接成完整的 m3u8 URL
  45. m3u8_url2 = m3u8_url;
  46.  
  47. // 創建複製鏈接的按鈕
  48. const copyLink = document.createElement('a');
  49. copyLink.classList.add('anig-tb');
  50. copyLink.textContent = `${key}p`;
  51. copyLink.title = '複製ffmpeg下載指令';
  52. copyLink.addEventListener('click', function () {
  53. let ffmpegUrl;
  54. if (mode==0){
  55. ffmpegUrl = `ffmpeg -headers "Origin: https://ani.gamer.com.tw" -i "${m3u8_url}" -c copy "${path}/${Name}.mkv" && exit`; // 構建 PotPlayer 協議的 URL
  56. }else{
  57. ffmpegUrl = `${m3u8_url}@${Name}.mkv"`; // 構建 PotPlayer 協議的 URL
  58. }
  59. navigator.clipboard.writeText(ffmpegUrl); // 複製鏈接
  60. titleDisplay.textContent = '複製成功!'; // 提示成功
  61. setTimeout(() => {
  62. titleDisplay.textContent = '複製下載指令或使用外部播放器'; // 恢復提示文字
  63. }, 500);
  64. });
  65. m3u8Container.appendChild(copyLink);
  66. }
  67.  
  68. // 創建使用 MPV 播放的按鈕
  69. const MPVLink = document.createElement('a');
  70. MPVLink.classList.add('anig-tb');
  71. MPVLink.textContent = 'MPV';
  72. MPVLink.title = '使用 MPV 播放';
  73. MPVLink.addEventListener('click', function () {
  74. const MPVUrl = `${m3u8_url2} --http-header-fields="origin: https://ani.gamer.com.tw" --force-media-title="${Name}"`; // 構建 MPV 協議的 URL
  75. navigator.clipboard.writeText(MPVUrl);
  76. window.open('mpv:', '_self'); // 開啟 PotPlayer
  77. });
  78. m3u8Container.appendChild(MPVLink);
  79.  
  80. // 創建使用 PotPlayer 播放的按鈕
  81. const potplayerLink = document.createElement('a');
  82. potplayerLink.classList.add('anig-tb');
  83. potplayerLink.textContent = 'PotPlayer';
  84. potplayerLink.title = '使用 PotPlayer 播放';
  85. potplayerLink.addEventListener('click', function () {
  86. const potplayerUrl = `${m3u8_url2} /sub="" /headers="origin: https://ani.gamer.com.tw" /current /title="${Name}"`; // 構建 PotPlayer 協議的 URL
  87. navigator.clipboard.writeText(potplayerUrl);
  88. window.open('potplayer:', '_self'); // 開啟 PotPlayer
  89. });
  90. m3u8Container.appendChild(potplayerLink);
  91.  
  92. }
  93.  
  94. /**
  95. * 獲取播放列表,並等待廣告結束
  96. */
  97. async function getPlaylist() {
  98. const req = `https://ani.gamer.com.tw/ajax/m3u8.php?sn=${AniVideoSn}&device=${DeviceID}`; // 構建請求 URL
  99. titleDisplay.textContent = '等待廣告...'; // 提示使用者等待廣告
  100.  
  101. let retries = 0; // 重試次數計數器
  102. const maxRetries = 20; // 最多嘗試次數,防止無限循環
  103.  
  104. // 循環請求播放列表,直到獲取到有效的播放地址或達到最大重試次數
  105. while (retries < maxRetries) {
  106. const response = await fetch(req); // 發送請求
  107. playlist = await response.json(); // 解析 JSON 資料
  108.  
  109. // 如果獲取到有效的播放地址(不包含廣告)
  110. if (playlist.src && playlist.src.includes('https')) {
  111. break; // 跳出循環
  112. }
  113.  
  114. await new Promise(resolve => setTimeout(resolve, 3000)); // 等待 3 秒再重試
  115. retries++; // 增加重試次數
  116. }
  117.  
  118. // 判斷是否成功獲取播放列表
  119. if (playlist.src && playlist.src.includes('https')) {
  120. await parsePlaylist(); // 解析播放列表並生成按鈕
  121. } else {
  122. titleDisplay.textContent = '獲取播放列表失敗'; // 提示使用者失敗
  123. }
  124. }
  125.  
  126. /**
  127. * 獲取設備 ID,這是請求播放列表所需的參數
  128. */
  129. async function getDeviceId() {
  130. const req = 'https://ani.gamer.com.tw/ajax/getdeviceid.php'; // 請求設備 ID 的 URL
  131. const response = await fetch(req); // 發送請求
  132. const data = await response.json(); // 解析 JSON 資料
  133. DeviceID = data.deviceid; // 提取設備 ID
  134. await getPlaylist(); // 繼續獲取播放列表
  135. }
  136.  
  137. // Main
  138.  
  139. // 從 URL 中獲取動畫的編號(AniVideoSn)
  140. let AniVideoSn = new URLSearchParams(window.location.search).get('sn');
  141. // 定義全域變數
  142. let DeviceID; // 設備 ID
  143. let playlist; // 播放列表資料
  144.  
  145. // 定義樣式
  146. const css =`
  147. .anig-ct {
  148. margin: 5px;
  149. }
  150.  
  151. .anig-tb {
  152. display: inline-block;
  153. padding: 5px;
  154. background: #50b2d7;
  155. color: #FFF;
  156. margin-right: 5px;
  157. cursor: pointer;
  158. }`;
  159.  
  160. // 創建顯示提示信息的元素
  161. const titleDisplay = document.createElement('div');
  162. titleDisplay.classList.add('anig-tb');
  163. titleDisplay.textContent = '載入中...'; // 初始提示文字
  164. // 創建一個容器來包裹提示信息
  165. const container = document.createElement('div');
  166. container.classList.add('anig-ct');
  167. container.appendChild(titleDisplay); // 將提示元素添加到容器中
  168. // 創建容器,用於放置清晰度按鈕和 PotPlayer 按鈕
  169. const m3u8Container = document.createElement('div');
  170. m3u8Container.classList.add('anig-ct');
  171. // 將容器添加到頁面中的指定位置
  172. const animeName = document.querySelector('.anime_name');
  173. animeName.appendChild(container); // 添加提示容器
  174. animeName.appendChild(m3u8Container); // 添加按鈕容器
  175.  
  176. /**
  177. * 為頁面中的集數連結添加點擊事件監聽
  178. * 當使用者點擊不同的集數時,更新 AniVideoSn 並重新獲取播放列表
  179. */
  180. document.querySelectorAll('a[data-ani-video-sn]').forEach(link => {
  181. link.addEventListener('click', function () {
  182. AniVideoSn = this.getAttribute('data-ani-video-sn'); // 更新 AniVideoSn
  183. m3u8Container.innerHTML = ''; // 清空按鈕容器
  184. titleDisplay.textContent = '載入中...'; // 更新提示文字
  185. getDeviceId(); // 重新獲取設備 ID 並獲取播放列表
  186. });
  187. });
  188.  
  189. /**
  190. * 新增檢查機制,監測 URL 和 AniVideoSn 是否發生變化
  191. * 如果發生變化,則重新獲取播放列表,確保資料的及時更新
  192. */
  193. let lastAniVideoSn = AniVideoSn; // 保存上一次的 AniVideoSn
  194. let lastUrl = window.location.href; // 保存上一次的 URL
  195.  
  196. setInterval(() => {
  197. const currentUrl = window.location.href; // 獲取當前的 URL
  198. const currentAniVideoSn = new URLSearchParams(window.location.search).get('sn'); // 獲取當前的 AniVideoSn
  199.  
  200. // 如果 URL 或 AniVideoSn 發生變化
  201. if (currentUrl !== lastUrl || currentAniVideoSn !== lastAniVideoSn) {
  202. lastUrl = currentUrl; // 更新 URL
  203. lastAniVideoSn = currentAniVideoSn; // 更新 AniVideoSn
  204. AniVideoSn = currentAniVideoSn; // 更新全域變數
  205.  
  206. m3u8Container.innerHTML = ''; // 清空按鈕容器
  207. titleDisplay.textContent = '載入中...'; // 更新提示文字
  208. getDeviceId(); // 重新獲取設備 ID 並獲取播放列表
  209. }
  210. }, 1000); // 每秒檢查一次
  211.  
  212. // 開始執行程式
  213. getDeviceId(); // 獲取設備 ID 並開始流程
  214. injectStyles(css); // 注入自定義樣式到頁面
  215.  
  216. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址