// ==UserScript==
// @name EPIC白嫖小助手
// @description 每1小时检测一次是否有可以白嫖的epic游戏
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.23
// @author CodFrm,Cosil
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_closeNotification
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM_setValue
// @storageName find_epic_free_games
// @connect store-site-backend-static.ak.epicgames.com
// @connect www.epicgames.com
// @crontab * once * * *
// @license GPLv3
// @match undefined
// ==/UserScript==
let url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=zh-Hant&country=CN&allowCountries=CN,HK";
console.log("开始执行检查 - " + new Date().toLocaleString());
function request(option) {
return new Promise((resolve, reject) => {
option.onload = (res) => {
if (res.status != 200) {
reject();
}
resolve(res)
};
option.onerror = () => { reject() };
GM_xmlhttpRequest(option);
})
}
function toDataURL(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
var reader = new FileReader();
reader.onloadend = function () {
// callback(reader.result);
resolve(reader.result)
}
reader.readAsDataURL(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
})
}
return new Promise((resolve, reject) => {
console.log("开始执行检查 - " + new Date().toLocaleString());
GM_xmlhttpRequest({
url: url,
responseType: "json",
onload: async (resp) => {
try {
if (resp.status != 200) {
GM_notification({
title: "Epic 检测失败",
text: "网站检测错误:" + resp.status + ",5分钟后重试"
});
// 5分钟后重试
reject(new CATRetryError("请求失败: " + resp.status, 300));
return;
}
let games = [];
let msg = ""
let elements = resp.response.data.Catalog.searchStore.elements;
let itemInLibrary = GM_getValue("item_in_library", {});
// console.log("get::item_in_library", itemInLibrary, Object.keys(itemInLibrary).length);
//超过10个清空存储
// itemInLibrary = Object.keys(itemInLibrary).length > 10 ? {} : itemInLibrary;
console.log("now_item_in_library", itemInLibrary);
for (const key in elements) {
//本身不免费,现在免费了
//2022年12月22日活动产品原价是0 elements[key].price.totalPrice.originalPrice &&
if (!elements[key].price.totalPrice.discountPrice) {
//Mystery Game 跳过神秘游戏 Mystery Game Day 4
// if (elements[key].title == "Mystery Game") {
if (elements[key].title.indexOf("Mystery Game") >= 0) {
continue;
}
if (new Date(elements[key].effectiveDate) > new Date()) {
//过滤还未发售的游戏
continue;
}
//输出游戏信息
console.log(elements[key].title, elements[key].status, Object.keys(itemInLibrary).includes(elements[key].id))
//活动还在且未购买
if (elements[key].status == "ACTIVE" && !Object.keys(itemInLibrary).includes(elements[key].id)) {//
msg += elements[key].title + "; "
let img = "";
let imagedata = elements[key].keyImages.find(elem => elem.pageType === "DieselStoreFrontWide");
if (!imagedata) {
imagedata = elements[key].keyImages[0];
}
if (imagedata) {
img = imagedata.url;
}
var productSlug = "";
if (elements[key].catalogNs.mappings && elements[key].catalogNs.mappings.find(elem => elem.pageType === "productHome")) {
productSlug = elements[key].catalogNs.mappings.find(elem => elem.pageType === "productHome").pageSlug;
}
else if (elements[key]["productSlug"]) {
productSlug = elements[key]["productSlug"];
} else {
GM_notification("epic白嫖失败,获取游戏链接失败!");
continue;
}
switch (elements[key].offerType) {
case "BUNDLE":
games.push({
title: elements[key].title,
url: "https://store.epicgames.com/zh-CN/bundles/" + productSlug,
id: elements[key].id,
image: img,
});
break;
default:
games.push({
title: elements[key].title,
url: "https://store.epicgames.com/zh-CN/p/" + productSlug,
id: elements[key].id,
image: img,
});
break;
}
}
}
}
console.log("found_games", games);
let parser = new DOMParser();
console.log("req_start");
await Promise.all(games.map(game => request({
url: game.url,
headers: {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6,zh-HK;q=0.5,ko;q=0.4,ru;q=0.3,ja;q=0.2",
"priority": "u=1, i",
"sec-ch-ua": "\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"x-requested-with": "XMLHttpRequest"
},
method: "GET",
referrer: game.url,
referrerPolicy: "no-referrer-when-downgrade",
mode: "cors",
credentials: "omit"
}))).then(resArr => {
console.log("req_end", resArr);
for (let i in resArr) {
var html = resArr[i].responseText;
var tempElement = document.createElement('div');
tempElement.innerHTML = html;
// 获取游戏描述等其他信息的代码...
var epicClientState = tempElement.querySelector('script').innerText.match(/window\.__REACT_QUERY_INITIAL_QUERIES__\s*=\s*({.*?});/);
if (epicClientState) {
var parsedEpicClientState = JSON.parse(epicClientState[1]);
console.log(parsedEpicClientState);
var getCatalogOffer = parsedEpicClientState.queries.filter(t => t.queryKey[0] == "getCatalogOffer");
if (getCatalogOffer && getCatalogOffer.length) {
games[i].description = getCatalogOffer[0].state.data.Catalog.catalogOffer.title + "_" + getCatalogOffer[0].state.data.Catalog.catalogOffer.description;
}
}
// 方法1:直接检查购买按钮状态
const purchaseButton = tempElement.querySelector('[data-testid="purchase-cta-button"] span span');
const isInLibrary = purchaseButton && purchaseButton.textContent.trim() === "已在库中";
// 方法2:备用检测方法(原有逻辑)
let match = /(?<="diesel.common.button.in_library"\s*:\s*")[^,"]+(?=",)/.exec(html);
let in_library_ctx = match ? match[0] : "\error";
let status = tempElement.querySelector("[data-testid=add-to-cart-cta-button]")?.innerText;
// 如果任一方法检测到游戏在库中,则标记为已拥有
if (isInLibrary || (in_library_ctx === status)) {
itemInLibrary[games[i].id] = games[i].title;
console.log("已在库中", games[i].title);
continue; // 跳过后续处理
}
}
})
//更新已购列表
GM_setValue("item_in_library", itemInLibrary);
console.log("update_value", GM_getValue("item_in_library"));
//删选已购买
games = games.filter(game => !Object.keys(itemInLibrary).includes(game.id));
if (!games.length) {
console.log("没有找到可以白嫖的游戏.....");
return resolve();
}
// console.log(games[0].image);
for (const key in games) { //转换为base64
try {
games[key].image = await toDataURL(games[key].image)
} catch {
games[key].image = "";
}
}
for (const key in games) {
GM_notification({
title: "今日白嫖名单-" + games[key].title,
text: games[key].description,
image: games[key].image,//TODO 图像下载失败会照成消息无法弹出
buttons: [{ title: "已白嫖,不在提示" }, { title: "马上去白嫖" }],//只能存在2个
onclick(id, btn) {
if (btn === 1) {
GM_openInTab(games[key].url);
} if (btn === 0) {//已白嫖,不在提示
itemInLibrary[games[key].id] = games[key].title;
//更新已购列表
GM_setValue("item_in_library", itemInLibrary);
}
GM_closeNotification(id);
resolve();
},
timeout: 20 * 1000,
ondone(click) {
if (!click) {
resolve();
}
}
});
await new Promise(resolve => setTimeout(resolve, 1000 * 3)); //等待3秒在弹出下一个
}
} catch (error) {
debugger
console.error("处理数据时出错:", error);
GM_notification({
title: "Epic 检测出错",
text: "处理数据时出错,3分钟后重试"
});
// 3分钟后重试
reject(new CATRetryError("处理数据出错: " + error.message, 180));
}
},
onerror: (error) => {
console.error("网络请求失败:", error);
GM_notification({
title: "Epic 网络错误",
text: "网络请求失败,1分钟后重试"
});
// 1分钟后重试
reject(new CATRetryError("网络请求失败", 60));
}
});
});