- // ==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.15
- // @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://gf.qytechs.cn/users/989635
-
- // @run-at document-body
- // @grant window.close
- // @grant GM_notification
- // ==/UserScript==
-
- (async () => {
- const Config = {
- RestartLive: true, // 使用重啟直播
- EndAutoClose: true, // 全部進度完成後自動關閉
- TryStayActive: true, // 嘗試讓頁面保持活躍
- RestartLiveMute: true, // 重啟的直播靜音
- RestartLowQuality: false, // 重啟直播最低畫質
-
- UpdateDisplay: true, // 於標題展示更新倒數
- ClearExpiration: true, // 清除過期的掉寶進度
- ProgressDisplay: true, // 於標題展示掉寶進度
-
- UpdateInterval: 120, // (seconds) 更新進度狀態的間隔
- JudgmentInterval: 6, // (Minute) 經過多長時間進度無增加, 就重啟直播 [設置太短會可能誤檢測]
-
- DropsButton: "button.ejeLlX", // 掉寶領取按鈕
- FindTag: ["drops", "啟用掉寶", "启用掉宝", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可
- };
- class Detection {
- constructor() {
- this.ProgressParse = progress => progress.sort((a, b) => b - a).find(number => number < 100);
- this.GetTime = () => {
- const time = this.CurrentTime;
- const year = time.getFullYear();
- const month = `${time.getMonth() + 1}`.padStart(2, "0");
- const date = `${time.getDate()}`.padStart(2, "0");
- const hour = `${time.getHours()}`.padStart(2, "0");
- const minute = `${time.getMinutes()}`.padStart(2, "0");
- const second = `${time.getSeconds()}`.padStart(2, "0");
- return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
- };
- 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),
- Object: parse => parse ? JSON.parse(parse) : (sessionStorage.setItem(key, JSON.stringify(value)),
- !0)
- };
- return value != null ? Formula[Formula.Type(value)]() : (data = sessionStorage.getItem(key),
- data != undefined ? Formula[Formula.Type(JSON.parse(data))](data) : data);
- };
- this.Adapter = {
- __ConvertPM: time => time.replace(/(\d{1,2}):(\d{2})/, (_, hours, minutes) => `${+hours + 12}:${minutes}`),
- "en-US": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
- "en-GB": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
- "es-ES": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
- "fr-FR": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
- "pt-PT": (timeStamp, currentYear) => {
- const convert = timeStamp.replace(/(\d{1,2})\/(\d{1,2})/, (_, day, month) => `${month}/${day}`);
- return new Date(`${convert} ${currentYear}`);
- },
- "pt-BR": (timeStamp, currentYear) => {
- const ISO = {
- 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"
- };
- 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()]);
- return new Date(`${convert} ${currentYear}`);
- },
- "ru-RU": (timeStamp, currentYear) => {
- const ISO = {
- "янв": "Jan", "фев": "Feb", "мар": "Mar", "апр": "Apr", "май": "May", "июн": "Jun", "июл": "Jul", "авг": "Aug", "сен": "Sep", "окт": "Oct", "ноя": "Nov", "дек": "Dec", "пн": "Mon", "вт": "Tue", "ср": "Wed", "чт": "Thu", "пт": "Fri", "сб": "Sat", "вс": "Sun"
- };
- const convert = timeStamp.replace(/(янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек|пн|вт|ср|чт|пт|сб|вс)/gi, match => ISO[match.toLowerCase()]);
- return new Date(`${convert} ${currentYear}`);
- },
- "de-DE": (timeStamp, currentYear) => {
- const ISO = {
- 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"
- };
- 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()]);
- return new Date(`${convert} ${currentYear}`);
- },
- "it-IT": (timeStamp, currentYear) => {
- const ISO = {
- 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"
- };
- 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()]);
- return new Date(`${convert} ${currentYear}`);
- },
- "tr-TR": (timeStamp, currentYear) => {
- const ISO = {
- 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"
- };
- 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()]);
- const match = convert.match(/(\d{1,2}) ([a-z]+) ([a-z]+) (\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/i);
- return new Date(`${match[3]} ${match[1]} ${match[2]} ${match[4]} ${match[5]} ${currentYear}`);
- },
- "es-MX": (timeStamp, currentYear) => {
- 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);
- const time = timeStamp.includes("p.m") ? this.Adapter.__ConvertPM(match[4]) : match[4];
- return new Date(`${match[1]}, ${match[2]} ${match[3]}, ${time} ${match[5]} ${currentYear}`);
- },
- "ja-JP": (timeStamp, currentYear) => {
- const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
- return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[3]}:00 ${match[4]}`);
- },
- "ko-KR": (timeStamp, currentYear) => {
- const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
- const time = timeStamp.includes("오후") ? this.Adapter.__ConvertPM(match[3]) : match[3];
- return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
- },
- "zh-TW": (timeStamp, currentYear) => {
- const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(\d{1,2}:\d{1,2}) \[(GMT[+-]\d{1,2})\]/);
- const time = timeStamp.includes("下午") ? this.Adapter.__ConvertPM(match[3]) : match[3];
- return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
- },
- "zh-CN": (timeStamp, currentYear) => {
- const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(GMT[+-]\d{1,2}) (\d{1,2}:\d{1,2})/);
- return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[4]}:00 ${match[3]}`);
- }
- };
- this.PageRefresh = async (display, interval) => {
- if (display) {
- setInterval(() => {
- document.title = `【 ${interval--}s 】 ${this.ProgressValue}`;
- }, 1e3);
- }
- setTimeout(() => {
- location.reload();
- }, (interval + 1) * 1e3);
- };
- this.ShowProgress = () => {
- new MutationObserver(() => {
- document.title != this.ProgressValue && (document.title = this.ProgressValue);
- }).observe(document.querySelector("title"), {
- childList: !0,
- subtree: !1
- });
- document.title = this.ProgressValue;
- };
- this.ExpiredCleanup = (Object, Adapter, Timestamp, Callback) => {
- const targetTime = Adapter?.(Timestamp, this.CurrentTime.getFullYear()) ?? this.CurrentTime;
- this.CurrentTime > targetTime ? this.Config.ClearExpiration && Object.remove() : Callback(Object);
- };
- this.ProgressValue = "";
- this.CurrentTime = new Date();
- this.Config = Object.assign(Config, {
- EndLine: "div.gtpIYu",
- AllProgress: "div.ilRKfU",
- ProgressBar: "p.mLvNZ span",
- ActivityTime: "span.jSkguG"
- });
- }
- static async Ran() {
- let Task = 0, Progress = 0, MaxElement = 0;
- const Progress_Info = {};
- const Detec = new Detection();
- const Self = Detec.Config;
- const Display = Self.UpdateDisplay;
- const Process = Token => {
- document.querySelectorAll(Self.DropsButton).forEach(draw => {
- draw.click();
- });
- if (!Self.RestartLive && !Self.EndAutoClose && !Self.ClearExpiration && !Self.ProgressDisplay) return;
- const All_Data = document.querySelectorAll(Self.AllProgress);
- if (All_Data && All_Data.length > 0) {
- const Adapter = Detec.Adapter[document.documentElement.lang];
- All_Data.forEach(data => {
- Detec.ExpiredCleanup(data, Adapter, data.querySelector(Self.ActivityTime).textContent, NotExpired => {
- Progress_Info[Task++] = [...NotExpired.querySelectorAll(Self.ProgressBar)].map(progress => +progress.textContent);
- });
- });
- const OldTask = Detec.Storage("Task") ?? {};
- const NewTask = Object.fromEntries(Object.entries(Progress_Info).map(([key, value]) => [key, Detec.ProgressParse(value)]));
- for (const [key, value] of Object.entries(NewTask)) {
- const OldValue = OldTask[key] ?? value;
- if (value != OldValue) {
- MaxElement = key;
- Progress = value;
- break;
- } else if (value > Progress) {
- MaxElement = key;
- Progress = value;
- }
- }
- Detec.Storage("Task", NewTask);
- }
- if (Progress > 0) {
- Detec.ProgressValue = `${Progress}%`;
- !Display && Detec.ShowProgress();
- } else if (Token > 0) {
- setTimeout(() => {
- Process(Token - 1);
- }, 2e3);
- }
- const [Record, Timestamp] = Detec.Storage("Record") ?? [0, Detec.GetTime()];
- const Diff = ~~((Detec.CurrentTime - new Date(Timestamp)) / (1e3 * 60));
- if (!Progress && Self.EndAutoClose && Record != 0 && Token == 0) {
- window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close();
- window.close();
- } else if (Diff >= Self.JudgmentInterval && Progress == Record) {
- Self.RestartLive && Restart.Ran(MaxElement);
- Detec.Storage("Record", [Progress, Detec.GetTime()]);
- } else if (Diff == 0 || Progress != Record) {
- if (Progress != 0) Detec.Storage("Record", [Progress, Detec.GetTime()]);
- }
- };
- WaitElem(document, Self.EndLine, () => {
- Process(4);
- Self.TryStayActive && StayActive(document);
- }, {
- timeoutResult: true
- });
- Detec.PageRefresh(Display, Self.UpdateInterval);
- }
- }
- class RestartLive {
- constructor() {
- this.LiveMute = async Newindow => {
- WaitElem(Newindow.document, "video", video => {
- const SilentInterval = setInterval(() => {
- video.muted = !0;
- }, 500);
- setTimeout(() => {
- clearInterval(SilentInterval);
- }, 15e3);
- });
- };
- this.LiveLowQuality = async Newindow => {
- const Dom = Newindow.document;
- WaitElem(Dom, "[data-a-target='player-settings-button']", Menu => {
- Menu.click();
- WaitElem(Dom, "[data-a-target='player-settings-menu-item-quality']", Quality => {
- Quality.click();
- WaitElem(Dom, "[data-a-target='player-settings-menu']", Settings => {
- Settings.lastElementChild.click();
- setTimeout(() => {
- Menu.click();
- }, 800);
- });
- });
- });
- };
- this.Config = Object.assign(Config, {
- TagType: "span",
- Article: "article",
- Offline: "p.fQYeyD",
- Online: "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(Index) {
- window.open("", "LiveWindow", "top=0,left=0,width=1,height=1").close();
- const Dir = this;
- const Self = Dir.Config;
- const FindTag = new RegExp(Self.FindTag.join("|"));
- let NewWindow, OpenLink, article;
- let Channel = document.querySelectorAll(Self.ActivityLink2)[Index];
- if (Channel) {
- NewWindow = window.open(Channel.href, "LiveWindow");
- DirectorySearch(NewWindow);
- } else {
- Channel = document.querySelectorAll(Self.ActivityLink1)[Index];
- 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(() => {
- Nowlive = NewWindow.document.querySelector(Self.Online);
- Offline = NewWindow.document.querySelector(Self.Offline);
- 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 > 10) {
- 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 Word = {
- Traditional: {},
- Simplified: {
- "搜尋失敗": "搜索失败",
- "找不到啟用掉落的頻道": "找不到启用掉落的频道"
- },
- Korea: {
- "搜尋失敗": "검색 실패",
- "找不到啟用掉落的頻道": "드롭이 활성화된 채널을 찾을 수 없습니다"
- },
- Japan: {
- "搜尋失敗": "検索失敗",
- "找不到啟用掉落的頻道": "ドロップが有効なチャンネルが見つかりません"
- },
- English: {
- "搜尋失敗": "Search failed",
- "找不到啟用掉落的頻道": "Can't find a channel with drops enabled"
- }
- }, Match = {
- ko: Word.Korea,
- ja: Word.Japan,
- "en-US": Word.English,
- "zh-CN": Word.Simplified,
- "zh-SG": Word.Simplified,
- "zh-TW": Word.Traditional,
- "zh-HK": Word.Traditional,
- "zh-MO": Word.Traditional
- }, ML = Match[lang] ?? Match["en-US"];
- return {
- Transl: Str => ML[Str] ?? Str
- };
- }
- const Lang = Language(navigator.language);
- GM_notification({
- title: Lang.Transl("搜尋失敗"),
- text: Lang.Transl("找不到啟用掉落的頻道")
- });
- }
- }
- }, 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 WaitElem(document, selector, found, {
- timeout = 1e4,
- throttle = 200,
- timeoutResult = false
- } = {}) {
- let timer, element;
- const observer = new MutationObserver(Throttle(() => {
- element = document.querySelector(selector);
- if (element) {
- observer.disconnect();
- clearTimeout(timer);
- found(element);
- }
- }, throttle));
- observer.observe(document, {
- subtree: !0,
- childList: !0,
- characterData: !0
- });
- timer = setTimeout(() => {
- observer.disconnect();
- timeoutResult && found(element);
- }, timeout);
- }
- 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} = e.data;
- fetch(url);
- postMessage({url});
- }, 1e4);
- }
- \`);
- Active.postMessage({ url: location.href});
- Active.onmessage = (e) => {
- const { url } = e.data;
- document.querySelector("video")?.play();
- Active.postMessage({ url: url});
- };
- `;
- Target.head.append(script);
- }
- const Restart = new RestartLive();
- Detection.Ran();
- })();