// ==UserScript== // @name Twitch 自動領取掉寶 / Auto Receive Drops // @name:zh-TW Twitch 自動領取掉寶 // @name:zh-CN Twitch 自动领取掉宝 // @name:en Twitch Auto Claim Drops // @name:ja Twitch 自動ドロップ受け取り // @name:ko Twitch 자동 드롭 수령 // @version 0.0.14 // @author Canaan HS // @description Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置 // @description:zh-TW Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置 // @description:zh-CN Twitch 自动领取 (掉宝/Drops) , 窗口标签显示进度 , 直播结束时还没领完 , 会自动寻找任意掉宝直播 , 并开启后继续挂机 , 代码自定义设置 // @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. // @description:ja Twitch のドロップを自動的に受け取り、タブに進捗狀況を表示し、ストリーム終了時にまだ受け取っていない場合、自動的に別のドロップ有効なストリームを検索し、収穫を続けます。コードでのカスタマイズ可能な設定 // @description:ko Twitch 드롭을 자동으로 받아오고 탭에 진행 상황을 표시하며, 스트림이 종료되었을 때 아직 완료되지 않았다면 자동으로 다른 드롭 활성 스트림을 찾아 계속 수집합니다. 코드에서 사용자 정의 설정 가능합니다 // @match https://www.twitch.tv/drops/inventory // @icon https://cdn-icons-png.flaticon.com/512/8214/8214044.png // @license MIT // @namespace https://greasyfork.org/users/989635 // @run-at document-body // @grant window.close // @grant GM_notification // @downloadURL none // ==/UserScript== (function () { const Config = { RestartLive: true, // 使用重啟直播 EndAutoClose: true, // 全部進度完成後自動關閉 TryStayActive: true, // 嘗試讓頁面保持活躍 RestartLiveMute: true, // 重啟的直播靜音 RestartLowQuality: false, // 重啟直播最低畫質 ClearExpiration: true, // 清除過期的掉寶進度 ProgressDisplay: true, // 於標題展示掉寶進度 UpdateInterval: 90, // (seconds) 更新進度狀態的間隔 JudgmentInterval: 5, // (Minute) 經過多長時間進度無增加, 就重啟直播 [設置太短會可能誤檢測] DropsButton: "button.caieTg", // 掉寶領取按鈕 FindTag: ["drops", "启用掉宝", "啟用掉寶", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可 }; class Detection { constructor() { this.storage = (key, value = null) => { let data, Formula = { Type: (parse) => Object.prototype.toString.call(parse).slice(8, -1), Number: (parse) => parse ? Number(parse) : (sessionStorage.setItem(key, JSON.stringify(value)), !0), Array: (parse) => parse ? JSON.parse(parse) : (sessionStorage.setItem(key, JSON.stringify(value)), !0), }; return null != value ? Formula[Formula.Type(value)]() : !!(data = sessionStorage.getItem(key)) && Formula[Formula.Type(JSON.parse(data))](data); }; this.TimeComparison = async (Object, Timestamp, Callback) => { const match = Timestamp.match( /(\d{1,2})\D+(\d{1,2})\D+\D+(\d{1,2}:\d{2}) \[GMT([+-]\d{1,2})\]/ ); if (match) { const month = parseInt(match[1], 10); const day = parseInt(match[2], 10); const timeString = match[3]; const offset = parseInt(match[4], 10); const currentTime = new Date(); const targetTime = new Date( currentTime.getFullYear(), month - 1, day ); targetTime.setHours(parseInt(timeString.split(":")[0], 10) + 12); targetTime.setMinutes(parseInt(timeString.split(":")[1], 10)); targetTime.setHours(targetTime.getHours() - offset); currentTime > targetTime ? this.config.ClearExpiration && Object.remove() : Callback(Object); } }; this.ProgressParse = (progress) => progress.sort((a, b) => b - a).find((number) => number < 100); this.ShowTitle = async (display) => { this.config.ProgressDisplay = !1; new MutationObserver(() => { document.title != display && (document.title = display); }).observe(document.querySelector("title"), { childList: !0, subtree: !1, }); document.title = display; }; this.config = Object.assign(Config, { EndLine: "div.gtpIYu", AllProgress: "div.ilRKfU", ProgressBar: "p.mLvNZ span", ActivityTime: "span.jSkguG", }); } static async Ran() { let All_Data, Progress_Belong = {}, PI = 0, PV = 0; let state, title, deal = !0, dynamic = new Detection(), self = dynamic.config, observer = new MutationObserver( Throttle(() => { if (deal) { All_Data = document.querySelectorAll(self.AllProgress); if (All_Data && All_Data.length > 0) { deal = !1; All_Data.forEach((data, index) => { dynamic.TimeComparison( data, data.querySelector(self.ActivityTime).textContent, (NotExpired) => { Progress_Belong[index] = [ ...NotExpired.querySelectorAll(self.ProgressBar), ].map((progress) => +progress.textContent); } ); }); for (const [key, value] of Object.entries(Progress_Belong)) { const cache = dynamic.ProgressParse(value); cache > PV && ((PV = cache), (PI = key)); } title = PV > 0 ? PV : !1; state = title ? (self.ProgressDisplay && dynamic.ShowTitle(`${title}%`), !0) : !1; } } if (self.RestartLive && state) { self.RestartLive = !1; const time = new Date(), [Progress, Timestamp] = dynamic.storage("Record") || [ title, time.getTime(), ], conversion = (time - Timestamp) / (1e3 * 60); if (conversion >= self.JudgmentInterval && title == Progress) { Restart.Ran(PI); dynamic.storage("Record", [title, time.getTime()]); } else if (conversion == 0 || title != Progress) { dynamic.storage("Record", [title, time.getTime()]); } } document.querySelectorAll(self.DropsButton).forEach((draw) => { draw.click(); }); if (document.querySelector(self.EndLine)) { observer.disconnect(); const count = dynamic.storage("NoProgressCount") || 0; if (title) { dynamic.storage("NoProgressCount", 0); } else if (count > 2) { dynamic.storage("NoProgressCount", 0); if (self.EndAutoClose) { window .open("", "LiveWindow", "top=0,left=0,width=1,height=1") .close(); window.close(); } } else { dynamic.storage("NoProgressCount", count + 1); } } }, 300) ); observer.observe(document, { subtree: !0, childList: !0, characterData: !0, }); self.TryStayActive && StayActive(document); } } class RestartLive { constructor() { this.WaitElem = async (Newindow, selector, found) => { let element; const observer = new MutationObserver( Throttle(() => { element = Newindow.document.querySelector(selector); element && (observer.disconnect(), found(element)); }, 200) ); observer.observe(Newindow.document, { subtree: !0, childList: !0, characterData: !0, }); }; this.LiveMute = async (Newindow) => { this.WaitElem(Newindow, "video", (video) => { const SilentInterval = setInterval(() => { video.muted = !0; }, 500); setTimeout(() => { clearInterval(SilentInterval); }, 15e3); }); }; this.LiveLowQuality = async (Newindow) => { this.WaitElem( Newindow, "[data-a-target='player-settings-button']", (Menu) => { Menu.click(); this.WaitElem( Newindow, "[data-a-target='player-settings-menu-item-quality']", (Quality) => { Quality.click(); this.WaitElem( Newindow, "[data-a-target='player-settings-menu']", (Settings) => { Settings.lastElementChild.click(); setTimeout(() => { Menu.click(); }, 800); } ); } ); } ); }; this.config = Object.assign(Config, { TagType: "span", Article: "article", OfflineTag: "p.fQYeyD", ViewersTag: "span.hERoTc", WatchLiveLink: "[data-a-target='preview-card-image-link']", ActivityLink1: "[data-test-selector='DropsCampaignInProgressDescription-hint-text-parent']", ActivityLink2: "[data-test-selector='DropsCampaignInProgressDescription-no-channels-hint-text']", }); } async Ran(CI) { window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close(); let NewWindow, OpenLink, Channel, article, self = this.config, FindTag = new RegExp(self.FindTag.join("|")), dir = this; Channel = document.querySelectorAll(self.ActivityLink2)[CI]; if (Channel) { NewWindow = window.open(Channel.href, "LiveWindow"); DirectorySearch(NewWindow); } else { Channel = document.querySelectorAll(self.ActivityLink1)[CI]; OpenLink = [...Channel.querySelectorAll("a")].reverse(); FindLive(0); async function FindLive(index) { if (OpenLink.length - 1 < index) { return !1; } const href = OpenLink[index].href; NewWindow = !NewWindow ? window.open(href, "LiveWindow") : (NewWindow.location.assign(href), NewWindow); if (href.includes("directory")) { DirectorySearch(NewWindow); } else { let Offline, Nowlive; const observer = new MutationObserver( Throttle(() => { Offline = NewWindow.document.querySelector(self.OfflineTag); Nowlive = NewWindow.document.querySelector(self.ViewersTag); if (Offline) { observer.disconnect(); FindLive(index + 1); } else if (Nowlive) { observer.disconnect(); self.RestartLiveMute && dir.LiveMute(NewWindow); self.TryStayActive && StayActive(NewWindow.document); self.RestartLowQuality && dir.LiveLowQuality(NewWindow); } }, 300) ); NewWindow.onload = () => { observer.observe(NewWindow.document, { subtree: !0, childList: !0, characterData: !0, }); }; } } } async function DirectorySearch(NewWindow) { const observer = new MutationObserver( Throttle(() => { article = NewWindow.document.getElementsByTagName(self.Article); if (article.length > 20) { observer.disconnect(); const index = [...article].findIndex((element) => { const Tag_box = element.querySelectorAll(self.TagType); return ( Tag_box.length > 0 && [...Tag_box].some((match) => FindTag.test(match.textContent.toLowerCase()) ) ); }); if (index != -1) { article[index].querySelector(self.WatchLiveLink).click(); self.RestartLiveMute && dir.LiveMute(NewWindow); self.TryStayActive && StayActive(NewWindow.document); self.RestartLowQuality && dir.LiveLowQuality(NewWindow); } else { function Language(lang) { const Display = { Simplified: { title: "搜索失败", text: "找不到启用掉落的频道", }, Traditional: { title: "搜尋失敗", text: "找不到啟用掉落的頻道", }, Korea: { title: "검색 실패", text: "드롭이 활성화된 채널을 찾을 수 없습니다", }, Japan: { title: "検索失敗", text: "ドロップが有効なチャンネルが見つかりません", }, English: { title: "Search failed", text: "Can't find a channel with drops enabled", }, }, Match = { ko: Display.Korea, ja: Display.Japan, "en-US": Display.English, "zh-CN": Display.Simplified, "zh-SG": Display.Simplified, "zh-TW": Display.Traditional, "zh-HK": Display.Traditional, "zh-MO": Display.Traditional, }; return Match[lang] || Match["en-US"]; } const show = Language(navigator.language); GM_notification({ title: show.title, text: show.text, }); } } }, 300) ); NewWindow.onload = () => { observer.observe(NewWindow.document, { subtree: !0, childList: !0, characterData: !0, }); }; } } } function Throttle(func, delay) { let lastTime = 0; return (...args) => { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; func(...args); } }; } async function StayActive(Target) { const script = document.createElement("script"); script.id = "Stay-Active"; script.textContent = ` function WorkerCreation(code) { const blob = new Blob([code], {type: "application/javascript"}); return new Worker(URL.createObjectURL(blob)); } const Active = WorkerCreation(\` onmessage = function(e) { setTimeout(()=> { const {url, visible} = e.data; visible == "hidden" && fetch(url); postMessage({url}); }, 6e4); } \`); Active.postMessage({ url: location.href, visible: document.visibilityState}); Active.onmessage = (e) => { const { url } = e.data; const video = document.querySelector("video"); video && video.play(); Active.postMessage({ url: url, visible: document.visibilityState }); }; `; Target.head.append(script); } setTimeout(() => { location.reload(); }, 1e3 * Config.UpdateInterval); const Restart = new RestartLive(); Detection.Ran(); })();