Twitch 自动领取掉宝

Twitch 自动领取 (掉宝/Drops) , 窗口标签显示进度 , 直播结束时还没领完 , 会自动寻找任意掉宝直播 , 并开启后继续挂机 , 代码自定义设置

目前为 2024-11-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Twitch 自動領取掉寶 / Auto Receive Drops
  3. // @name:zh-TW Twitch 自動領取掉寶
  4. // @name:zh-CN Twitch 自动领取掉宝
  5. // @name:en Twitch Auto Claim Drops
  6. // @name:ja Twitch 自動ドロップ受け取り
  7. // @name:ko Twitch 자동 드롭 수령
  8. // @version 0.0.15
  9. // @author Canaan HS
  10. // @description Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
  11. // @description:zh-TW Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
  12. // @description:zh-CN Twitch 自动领取 (掉宝/Drops) , 窗口标签显示进度 , 直播结束时还没领完 , 会自动寻找任意掉宝直播 , 并开启后继续挂机 , 代码自定义设置
  13. // @description:en Automatically claim Twitch Drops, display progress in the tab, and if not finished when the stream ends, it will automatically find another Drops-enabled stream and continue farming. Customizable settings in the code.
  14. // @description:ja Twitch のドロップを自動的に受け取り、タブに進捗狀況を表示し、ストリーム終了時にまだ受け取っていない場合、自動的に別のドロップ有効なストリームを検索し、収穫を続けます。コードでのカスタマイズ可能な設定
  15. // @description:ko Twitch 드롭을 자동으로 받아오고 탭에 진행 상황을 표시하며, 스트림이 종료되었을 때 아직 완료되지 않았다면 자동으로 다른 드롭 활성 스트림을 찾아 계속 수집합니다. 코드에서 사용자 정의 설정 가능합니다
  16.  
  17. // @match https://www.twitch.tv/drops/inventory
  18. // @icon https://cdn-icons-png.flaticon.com/512/8214/8214044.png
  19.  
  20. // @license MIT
  21. // @namespace https://gf.qytechs.cn/users/989635
  22.  
  23. // @run-at document-body
  24. // @grant window.close
  25. // @grant GM_notification
  26. // ==/UserScript==
  27.  
  28. (async () => {
  29. const Config = {
  30. RestartLive: true, // 使用重啟直播
  31. EndAutoClose: true, // 全部進度完成後自動關閉
  32. TryStayActive: true, // 嘗試讓頁面保持活躍
  33. RestartLiveMute: true, // 重啟的直播靜音
  34. RestartLowQuality: false, // 重啟直播最低畫質
  35.  
  36. UpdateDisplay: true, // 於標題展示更新倒數
  37. ClearExpiration: true, // 清除過期的掉寶進度
  38. ProgressDisplay: true, // 於標題展示掉寶進度
  39.  
  40. UpdateInterval: 120, // (seconds) 更新進度狀態的間隔
  41. JudgmentInterval: 6, // (Minute) 經過多長時間進度無增加, 就重啟直播 [設置太短會可能誤檢測]
  42.  
  43. DropsButton: "button.ejeLlX", // 掉寶領取按鈕
  44. FindTag: ["drops", "啟用掉寶", "启用掉宝", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可
  45. };
  46. class Detection {
  47. constructor() {
  48. this.ProgressParse = progress => progress.sort((a, b) => b - a).find(number => number < 100);
  49. this.GetTime = () => {
  50. const time = this.CurrentTime;
  51. const year = time.getFullYear();
  52. const month = `${time.getMonth() + 1}`.padStart(2, "0");
  53. const date = `${time.getDate()}`.padStart(2, "0");
  54. const hour = `${time.getHours()}`.padStart(2, "0");
  55. const minute = `${time.getMinutes()}`.padStart(2, "0");
  56. const second = `${time.getSeconds()}`.padStart(2, "0");
  57. return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
  58. };
  59. this.Storage = (key, value = null) => {
  60. let data, Formula = {
  61. Type: parse => Object.prototype.toString.call(parse).slice(8, -1),
  62. Number: parse => parse ? Number(parse) : (sessionStorage.setItem(key, JSON.stringify(value)),
  63. !0),
  64. Array: parse => parse ? JSON.parse(parse) : (sessionStorage.setItem(key, JSON.stringify(value)),
  65. !0),
  66. Object: parse => parse ? JSON.parse(parse) : (sessionStorage.setItem(key, JSON.stringify(value)),
  67. !0)
  68. };
  69. return value != null ? Formula[Formula.Type(value)]() : (data = sessionStorage.getItem(key),
  70. data != undefined ? Formula[Formula.Type(JSON.parse(data))](data) : data);
  71. };
  72. this.Adapter = {
  73. __ConvertPM: time => time.replace(/(\d{1,2}):(\d{2})/, (_, hours, minutes) => `${+hours + 12}:${minutes}`),
  74. "en-US": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
  75. "en-GB": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
  76. "es-ES": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
  77. "fr-FR": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
  78. "pt-PT": (timeStamp, currentYear) => {
  79. const convert = timeStamp.replace(/(\d{1,2})\/(\d{1,2})/, (_, day, month) => `${month}/${day}`);
  80. return new Date(`${convert} ${currentYear}`);
  81. },
  82. "pt-BR": (timeStamp, currentYear) => {
  83. const ISO = {
  84. jan: "Jan", fev: "Feb", mar: "Mar", abr: "Apr", mai: "May", jun: "Jun", jul: "Jul", ago: "Aug", set: "Sep", out: "Oct", nov: "Nov", dez: "Dec", dom: "Sun", seg: "Mon", ter: "Tue", qua: "Wed", qui: "Thu", sex: "Fri", "sáb": "Sat"
  85. };
  86. const convert = timeStamp.replace(/de/g, "").replace(/(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez|dom|seg|ter|qua|qui|sex|sáb)/gi, match => ISO[match.toLowerCase()]);
  87. return new Date(`${convert} ${currentYear}`);
  88. },
  89. "ru-RU": (timeStamp, currentYear) => {
  90. const ISO = {
  91. "янв": "Jan", "фев": "Feb", "мар": "Mar", "апр": "Apr", "май": "May", "июн": "Jun", "июл": "Jul", "авг": "Aug", "сен": "Sep", "окт": "Oct", "ноя": "Nov", "дек": "Dec", "пн": "Mon", "вт": "Tue", "ср": "Wed", "чт": "Thu", "пт": "Fri", "сб": "Sat", "вс": "Sun"
  92. };
  93. const convert = timeStamp.replace(/(янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек|пн|вт|ср|чт|пт|сб|вс)/gi, match => ISO[match.toLowerCase()]);
  94. return new Date(`${convert} ${currentYear}`);
  95. },
  96. "de-DE": (timeStamp, currentYear) => {
  97. const ISO = {
  98. jan: "Jan", feb: "Feb", "mär": "Mar", apr: "Apr", mai: "May", jun: "Jun", jul: "Jul", aug: "Aug", sep: "Sep", okt: "Oct", nov: "Nov", dez: "Dec", mo: "Mon", di: "Tue", mi: "Wed", do: "Thu", fr: "Fri", sa: "Sat", so: "Sun"
  99. };
  100. const convert = timeStamp.replace(/(jan|feb|mär|apr|mai|jun|jul|aug|sep|okt|nov|dez|mo|di|mi|do|fr|sa|so)/gi, match => ISO[match.toLowerCase()]);
  101. return new Date(`${convert} ${currentYear}`);
  102. },
  103. "it-IT": (timeStamp, currentYear) => {
  104. const ISO = {
  105. gen: "Jan", feb: "Feb", mar: "Mar", apr: "Apr", mag: "May", giu: "Jun", lug: "Jul", ago: "Aug", set: "Sep", ott: "Oct", nov: "Nov", dic: "Dec", dom: "Sun", lun: "Mon", mar: "Tue", mer: "Wed", gio: "Thu", ven: "Fri", sab: "Sat"
  106. };
  107. const convert = timeStamp.replace(/(gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic|dom|lun|mar|mer|gio|ven|sab)/gi, match => ISO[match.toLowerCase()]);
  108. return new Date(`${convert} ${currentYear}`);
  109. },
  110. "tr-TR": (timeStamp, currentYear) => {
  111. const ISO = {
  112. oca: "Jan", "şub": "Feb", mar: "Mar", nis: "Apr", may: "May", haz: "Jun", tem: "Jul", "ağu": "Aug", eyl: "Sep", eki: "Oct", kas: "Nov", ara: "Dec", paz: "Sun", pts: "Mon", sal: "Tue", "çar": "Wed", per: "Thu", cum: "Fri", cmt: "Sat"
  113. };
  114. const convert = timeStamp.replace(/(oca|şub|mar|nis|may|haz|tem|ağu|eyl|eki|kas|ara|paz|pts|sal|çar|per|cum|cmt)/gi, match => ISO[match.toLowerCase()]);
  115. const match = convert.match(/(\d{1,2}) ([a-z]+) ([a-z]+) (\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/i);
  116. return new Date(`${match[3]} ${match[1]} ${match[2]} ${match[4]} ${match[5]} ${currentYear}`);
  117. },
  118. "es-MX": (timeStamp, currentYear) => {
  119. const match = timeStamp.match(/^([a-zñáéíóúü]+) (\d{1,2}) de ([a-zñáéíóúü]+), (\d{1,2}:\d{1,2}) (?:[ap]\.m\.) (GMT[+-]\d{1,2})/i);
  120. const time = timeStamp.includes("p.m") ? this.Adapter.__ConvertPM(match[4]) : match[4];
  121. return new Date(`${match[1]}, ${match[2]} ${match[3]}, ${time} ${match[5]} ${currentYear}`);
  122. },
  123. "ja-JP": (timeStamp, currentYear) => {
  124. const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
  125. return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[3]}:00 ${match[4]}`);
  126. },
  127. "ko-KR": (timeStamp, currentYear) => {
  128. const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
  129. const time = timeStamp.includes("오후") ? this.Adapter.__ConvertPM(match[3]) : match[3];
  130. return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
  131. },
  132. "zh-TW": (timeStamp, currentYear) => {
  133. const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(\d{1,2}:\d{1,2}) \[(GMT[+-]\d{1,2})\]/);
  134. const time = timeStamp.includes("下午") ? this.Adapter.__ConvertPM(match[3]) : match[3];
  135. return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
  136. },
  137. "zh-CN": (timeStamp, currentYear) => {
  138. const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(GMT[+-]\d{1,2}) (\d{1,2}:\d{1,2})/);
  139. return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[4]}:00 ${match[3]}`);
  140. }
  141. };
  142. this.PageRefresh = async (display, interval) => {
  143. if (display) {
  144. setInterval(() => {
  145. document.title = `【 ${interval--}s ${this.ProgressValue}`;
  146. }, 1e3);
  147. }
  148. setTimeout(() => {
  149. location.reload();
  150. }, (interval + 1) * 1e3);
  151. };
  152. this.ShowProgress = () => {
  153. new MutationObserver(() => {
  154. document.title != this.ProgressValue && (document.title = this.ProgressValue);
  155. }).observe(document.querySelector("title"), {
  156. childList: !0,
  157. subtree: !1
  158. });
  159. document.title = this.ProgressValue;
  160. };
  161. this.ExpiredCleanup = (Object, Adapter, Timestamp, Callback) => {
  162. const targetTime = Adapter?.(Timestamp, this.CurrentTime.getFullYear()) ?? this.CurrentTime;
  163. this.CurrentTime > targetTime ? this.Config.ClearExpiration && Object.remove() : Callback(Object);
  164. };
  165. this.ProgressValue = "";
  166. this.CurrentTime = new Date();
  167. this.Config = Object.assign(Config, {
  168. EndLine: "div.gtpIYu",
  169. AllProgress: "div.ilRKfU",
  170. ProgressBar: "p.mLvNZ span",
  171. ActivityTime: "span.jSkguG"
  172. });
  173. }
  174. static async Ran() {
  175. let Task = 0, Progress = 0, MaxElement = 0;
  176. const Progress_Info = {};
  177. const Detec = new Detection();
  178. const Self = Detec.Config;
  179. const Display = Self.UpdateDisplay;
  180. const Process = Token => {
  181. document.querySelectorAll(Self.DropsButton).forEach(draw => {
  182. draw.click();
  183. });
  184. if (!Self.RestartLive && !Self.EndAutoClose && !Self.ClearExpiration && !Self.ProgressDisplay) return;
  185. const All_Data = document.querySelectorAll(Self.AllProgress);
  186. if (All_Data && All_Data.length > 0) {
  187. const Adapter = Detec.Adapter[document.documentElement.lang];
  188. All_Data.forEach(data => {
  189. Detec.ExpiredCleanup(data, Adapter, data.querySelector(Self.ActivityTime).textContent, NotExpired => {
  190. Progress_Info[Task++] = [...NotExpired.querySelectorAll(Self.ProgressBar)].map(progress => +progress.textContent);
  191. });
  192. });
  193. const OldTask = Detec.Storage("Task") ?? {};
  194. const NewTask = Object.fromEntries(Object.entries(Progress_Info).map(([key, value]) => [key, Detec.ProgressParse(value)]));
  195. for (const [key, value] of Object.entries(NewTask)) {
  196. const OldValue = OldTask[key] ?? value;
  197. if (value != OldValue) {
  198. MaxElement = key;
  199. Progress = value;
  200. break;
  201. } else if (value > Progress) {
  202. MaxElement = key;
  203. Progress = value;
  204. }
  205. }
  206. Detec.Storage("Task", NewTask);
  207. }
  208. if (Progress > 0) {
  209. Detec.ProgressValue = `${Progress}%`;
  210. !Display && Detec.ShowProgress();
  211. } else if (Token > 0) {
  212. setTimeout(() => {
  213. Process(Token - 1);
  214. }, 2e3);
  215. }
  216. const [Record, Timestamp] = Detec.Storage("Record") ?? [0, Detec.GetTime()];
  217. const Diff = ~~((Detec.CurrentTime - new Date(Timestamp)) / (1e3 * 60));
  218. if (!Progress && Self.EndAutoClose && Record != 0 && Token == 0) {
  219. window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close();
  220. window.close();
  221. } else if (Diff >= Self.JudgmentInterval && Progress == Record) {
  222. Self.RestartLive && Restart.Ran(MaxElement);
  223. Detec.Storage("Record", [Progress, Detec.GetTime()]);
  224. } else if (Diff == 0 || Progress != Record) {
  225. if (Progress != 0) Detec.Storage("Record", [Progress, Detec.GetTime()]);
  226. }
  227. };
  228. WaitElem(document, Self.EndLine, () => {
  229. Process(4);
  230. Self.TryStayActive && StayActive(document);
  231. }, {
  232. timeoutResult: true
  233. });
  234. Detec.PageRefresh(Display, Self.UpdateInterval);
  235. }
  236. }
  237. class RestartLive {
  238. constructor() {
  239. this.LiveMute = async Newindow => {
  240. WaitElem(Newindow.document, "video", video => {
  241. const SilentInterval = setInterval(() => {
  242. video.muted = !0;
  243. }, 500);
  244. setTimeout(() => {
  245. clearInterval(SilentInterval);
  246. }, 15e3);
  247. });
  248. };
  249. this.LiveLowQuality = async Newindow => {
  250. const Dom = Newindow.document;
  251. WaitElem(Dom, "[data-a-target='player-settings-button']", Menu => {
  252. Menu.click();
  253. WaitElem(Dom, "[data-a-target='player-settings-menu-item-quality']", Quality => {
  254. Quality.click();
  255. WaitElem(Dom, "[data-a-target='player-settings-menu']", Settings => {
  256. Settings.lastElementChild.click();
  257. setTimeout(() => {
  258. Menu.click();
  259. }, 800);
  260. });
  261. });
  262. });
  263. };
  264. this.Config = Object.assign(Config, {
  265. TagType: "span",
  266. Article: "article",
  267. Offline: "p.fQYeyD",
  268. Online: "span.hERoTc",
  269. WatchLiveLink: "[data-a-target='preview-card-image-link']",
  270. ActivityLink1: "[data-test-selector='DropsCampaignInProgressDescription-hint-text-parent']",
  271. ActivityLink2: "[data-test-selector='DropsCampaignInProgressDescription-no-channels-hint-text']"
  272. });
  273. }
  274. async Ran(Index) {
  275. window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close();
  276. const Dir = this;
  277. const Self = Dir.Config;
  278. const FindTag = new RegExp(Self.FindTag.join("|"));
  279. let NewWindow, OpenLink, article;
  280. let Channel = document.querySelectorAll(Self.ActivityLink2)[Index];
  281. if (Channel) {
  282. NewWindow = window.open(Channel.href, "LiveWindow");
  283. DirectorySearch(NewWindow);
  284. } else {
  285. Channel = document.querySelectorAll(Self.ActivityLink1)[Index];
  286. OpenLink = [...Channel.querySelectorAll("a")].reverse();
  287. FindLive(0);
  288. async function FindLive(index) {
  289. if (OpenLink.length - 1 < index) return !1;
  290. const href = OpenLink[index].href;
  291. NewWindow = !NewWindow ? window.open(href, "LiveWindow") : (NewWindow.location.assign(href),
  292. NewWindow);
  293. if (href.includes("directory")) {
  294. DirectorySearch(NewWindow);
  295. } else {
  296. let Offline, Nowlive;
  297. const observer = new MutationObserver(Throttle(() => {
  298. Nowlive = NewWindow.document.querySelector(Self.Online);
  299. Offline = NewWindow.document.querySelector(Self.Offline);
  300. if (Offline) {
  301. observer.disconnect();
  302. FindLive(index + 1);
  303. } else if (Nowlive) {
  304. observer.disconnect();
  305. Self.RestartLiveMute && Dir.LiveMute(NewWindow);
  306. Self.TryStayActive && StayActive(NewWindow.document);
  307. Self.RestartLowQuality && Dir.LiveLowQuality(NewWindow);
  308. }
  309. }, 300));
  310. NewWindow.onload = () => {
  311. observer.observe(NewWindow.document, {
  312. subtree: !0,
  313. childList: !0,
  314. characterData: !0
  315. });
  316. };
  317. }
  318. }
  319. }
  320. async function DirectorySearch(NewWindow) {
  321. const observer = new MutationObserver(Throttle(() => {
  322. article = NewWindow.document.getElementsByTagName(Self.Article);
  323. if (article.length > 10) {
  324. observer.disconnect();
  325. const index = [...article].findIndex(element => {
  326. const Tag_box = element.querySelectorAll(Self.TagType);
  327. return Tag_box.length > 0 && [...Tag_box].some(match => FindTag.test(match.textContent.toLowerCase()));
  328. });
  329. if (index != -1) {
  330. article[index].querySelector(Self.WatchLiveLink).click();
  331. Self.RestartLiveMute && Dir.LiveMute(NewWindow);
  332. Self.TryStayActive && StayActive(NewWindow.document);
  333. Self.RestartLowQuality && Dir.LiveLowQuality(NewWindow);
  334. } else {
  335. function Language(lang) {
  336. const Word = {
  337. Traditional: {},
  338. Simplified: {
  339. "搜尋失敗": "搜索失败",
  340. "找不到啟用掉落的頻道": "找不到启用掉落的频道"
  341. },
  342. Korea: {
  343. "搜尋失敗": "검색 실패",
  344. "找不到啟用掉落的頻道": "드롭이 활성화된 채널을 찾을 수 없습니다"
  345. },
  346. Japan: {
  347. "搜尋失敗": "検索失敗",
  348. "找不到啟用掉落的頻道": "ドロップが有効なチャンネルが見つかりません"
  349. },
  350. English: {
  351. "搜尋失敗": "Search failed",
  352. "找不到啟用掉落的頻道": "Can't find a channel with drops enabled"
  353. }
  354. }, Match = {
  355. ko: Word.Korea,
  356. ja: Word.Japan,
  357. "en-US": Word.English,
  358. "zh-CN": Word.Simplified,
  359. "zh-SG": Word.Simplified,
  360. "zh-TW": Word.Traditional,
  361. "zh-HK": Word.Traditional,
  362. "zh-MO": Word.Traditional
  363. }, ML = Match[lang] ?? Match["en-US"];
  364. return {
  365. Transl: Str => ML[Str] ?? Str
  366. };
  367. }
  368. const Lang = Language(navigator.language);
  369. GM_notification({
  370. title: Lang.Transl("搜尋失敗"),
  371. text: Lang.Transl("找不到啟用掉落的頻道")
  372. });
  373. }
  374. }
  375. }, 300));
  376. NewWindow.onload = () => {
  377. observer.observe(NewWindow.document, {
  378. subtree: !0,
  379. childList: !0,
  380. characterData: !0
  381. });
  382. };
  383. }
  384. }
  385. }
  386. function Throttle(func, delay) {
  387. let lastTime = 0;
  388. return (...args) => {
  389. const now = Date.now();
  390. if (now - lastTime >= delay) {
  391. lastTime = now;
  392. func(...args);
  393. }
  394. };
  395. }
  396. async function WaitElem(document, selector, found, {
  397. timeout = 1e4,
  398. throttle = 200,
  399. timeoutResult = false
  400. } = {}) {
  401. let timer, element;
  402. const observer = new MutationObserver(Throttle(() => {
  403. element = document.querySelector(selector);
  404. if (element) {
  405. observer.disconnect();
  406. clearTimeout(timer);
  407. found(element);
  408. }
  409. }, throttle));
  410. observer.observe(document, {
  411. subtree: !0,
  412. childList: !0,
  413. characterData: !0
  414. });
  415. timer = setTimeout(() => {
  416. observer.disconnect();
  417. timeoutResult && found(element);
  418. }, timeout);
  419. }
  420. async function StayActive(Target) {
  421. const script = document.createElement("script");
  422. script.id = "Stay-Active";
  423. script.textContent = `
  424. function WorkerCreation(code) {
  425. const blob = new Blob([code], {type: "application/javascript"});
  426. return new Worker(URL.createObjectURL(blob));
  427. }
  428. const Active = WorkerCreation(\`
  429. onmessage = function(e) {
  430. setTimeout(()=> {
  431. const {url} = e.data;
  432. fetch(url);
  433. postMessage({url});
  434. }, 1e4);
  435. }
  436. \`);
  437. Active.postMessage({ url: location.href});
  438. Active.onmessage = (e) => {
  439. const { url } = e.data;
  440. document.querySelector("video")?.play();
  441. Active.postMessage({ url: url});
  442. };
  443. `;
  444. Target.head.append(script);
  445. }
  446. const Restart = new RestartLive();
  447. Detection.Ran();
  448. })();

QingJ © 2025

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