// ==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
// ==/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);
})();