// ==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.9 // @author HentaiSaru // @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-end // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== (function() { const Config = { RestartLive: true, // 使用重啟直播 RestartLiveMute: true, // 重啟的直播靜音 ProgressDisplay: true, // 於標題展示掉寶進度 InjectDelay: 1, // (seconds) 開始注入檢測的延遲 [提高降低性能消耗, 但太高會找不到] UpdateInterval: 120, // (seconds) 更新進度狀態的間隔 JudgmentInterval: 5, // (Minute) 判斷經過多長時間, 進度無增加, 就重啟直播 [設置太短會可能誤檢測] FindTag: ["drops", "启用掉宝", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可 ProgressBar: "p.CoreText-sc-1txzju1-0.mLvNZ span.CoreText-sc-1txzju1-0", // 掉寶進度數據 ReceiveDropsButton: ".ScCoreButton-sc-ocjdkq-0.ScCoreButtonPrimary-sc-ocjdkq-1.caieTg.eHSNkH", // 領取按鈕 TagType: ".ScTruncateText-sc-i3kjgq-0.ickTbV span", // 頻道 Tag 標籤 LiveLink: "[data-a-target='preview-card-image-link']", // 直播連結 LiveChannel: "[data-test-selector='DropsCampaignInProgressDescription-no-channels-hint-text']" // 直播頻道列表 } class Detection { #ProgressParse; #ShowTitle; constructor() { this.config = Config; this.#ProgressParse = progress => { progress.sort((a, b) => b - a); return progress.find(number => number < 100); } this.#ShowTitle = async display => { this.config.ProgressDisplay = false; const TitleDisplay = setInterval(()=>{ document.title !== display ? document.title = display : null; }, 300); setTimeout(()=> {clearInterval(TitleDisplay)}, 1000 * 8); } } static async Ran() { let Withdraw, state, title, data=[], use=true, dynamic = new Detection(), self = dynamic.config; const observer = new MutationObserver(() => { title = document.querySelectorAll(self.ProgressBar); title = title.length > 0 && use ? (use = false, title.forEach(progress=> data.push(+progress.textContent)), dynamic.#ProgressParse(data)) : false; state = self.ProgressDisplay && title != false ? (dynamic.#ShowTitle(`${title}%`), true) : false; if (self.RestartLive && state) { self.RestartLive = false; const time = new Date(), [Timestamp, Progress] = GM_getValue("record", null) || [time.getTime(), title], conversion = (time - Timestamp) / (1000 * 60); if (conversion >= self.JudgmentInterval && title === Progress) { Restart.Ran(); GM_setValue("record", [time.getTime(), title]); } else if (conversion === 0 || title !== Progress) { GM_setValue("record", [time.getTime(), title]); } } Withdraw = document.querySelector(self.ReceiveDropsButton); Withdraw ? (observer.disconnect(), Withdraw.click()) : null; }); setTimeout(()=> {observer.observe(document.body, {childList: true, subtree: true})}, 1000 * self.InjectDelay); } } class RestartLive { #VideoMute; constructor() { this.config = Config; this.#VideoMute = async window => { const Interval = setInterval(() => { let video = window.document.querySelector("video"); if (video) { clearInterval(Interval); const SilentInterval = setInterval(() => {video.muted = true}, 500); setTimeout(()=> {clearInterval(SilentInterval)}, 1000 * 8); } }, 1000); } } async Ran() { let NewWindow, article, channel, self = this.config; channel = document.querySelector(self.LiveChannel); if (channel) { window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close(); NewWindow = window.open(channel.href, "LiveWindow"); const Interval = setInterval(() => { article = NewWindow.document.getElementsByTagName("article"); if (article.length > 20) { clearInterval(Interval); const index = Array.from(article).findIndex(element => { const tag = element.querySelector(self.TagType).textContent.toLowerCase(); return self.FindTag.some(match=> tag.includes(match.toLowerCase())); }); article[index].querySelector(self.LiveLink).click(); self.RestartLiveMute ? this.#VideoMute(NewWindow) : null; } }, 500); } } } const Restart = new RestartLive(); Detection.Ran(); setTimeout(()=> {location.reload()}, 1000 * Config.UpdateInterval); })();