// ==UserScript==
// @name Steam 家庭库已有游戏标记
// @namespace http://tampermonkey.net/
// @version 2024-04-08
// @description 能够自动扫描你的家庭库库存,并在Steam游戏页面标记,并支持一键安装游戏
// @author Cliencer Goge
// @match https://store.steampowered.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=steampowered.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @license GPLv3
// ==/UserScript==
var dialog,appid,observer,isupdate
var saves = readstorage()
//console.log(saves)
const url = window.location.pathname;
var access_token,steamid
if(g_AccountID != 0){
access_token = JSON.parse(application_config.getAttribute("data-store_user_config")).webapi_token
steamid = JSON.parse(application_config.getAttribute("data-userinfo")).steamid
}
(function() {
'use strict';
init()
if(g_AccountID == 0){return;}
if(url=='/account/familymanagement' && saves.isStartDump){
observer_1();
}else{
if(saves.settings.isAutoScan && g_ServerTime-saves.lastupDateTime>86400){
scan(false)
}
if(!isupdate && g_ServerTime-saves.lastupDateTime>604800){
let innerText
if(saves.familyGameList.GameList.length == 0){
innerText="您似乎没有家庭库的游戏记录,是否现在扫描家庭库游戏并记录呢?"
}else{
innerText="您已经超过1周没有更新家庭库的游戏列表了,是否现在去扫描?"
}
ShowConfirmDialog('脚本提示',innerText,'扫描家庭库','取消').done(()=>{scan(true)}).fail(()=>{
ShowAlertDialog('脚本提示','如果需要手动扫描,可以在Steam主页右上角进入进行扫描','好的')
})
}
var search_suggestion = document.getElementById('search_suggestion_contents')
var observer_search = new MutationObserver((mutations, obs) => {
mutations.forEach(function(mutation) {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
// 确保是元素节点
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查新节点是否有指定的类
if (node.classList.contains('match_app')) {
addflag(node)
}
}
});
}
})
});
observer_search.observe(search_suggestion, {childList: true, subtree: true});
}
if(url == "/"){
observer_3()
observer = new MutationObserver((mutations, obs) => {
mutations.forEach(function(mutation) {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
// 确保是元素节点
if (node.nodeType === Node.ELEMENT_NODE) {
if(node.classList.contains('live_streams_ctn')){return;}
node.querySelectorAll("div").forEach((node)=>{
addflag(node)
})
node.querySelectorAll("a").forEach((node)=>{
if(node.classList.contains('screenshot')){return;}
if(node.querySelector('div.broadcast_live_stream_icon')){return;}
addflag(node)
})
}
})
}
})
})
observer.observe(document, {childList: true, subtree: true});
}
if(url.startsWith('/app/')&&g_AccountID != 0){
//addBanner(document.querySelector('div.block.game_media_and_summary_ctn'))
observer_2();
}
if(url.startsWith('/search/')&&g_AccountID != 0){
observer_4()
var search_results = document.getElementById('search_results')
observer = new MutationObserver((mutations, obs) => {
mutations.forEach(function(mutation) {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
// 确保是元素节点
if (node.nodeType === Node.ELEMENT_NODE) {
console.log(node)
if (node.classList.contains('search_result_row') && node.classList.contains('ds_collapse_flag') && !node.classList.contains('ds_owned')) {
addflag(node,"clear: left;")
}else{
let lists = node.querySelectorAll("a.search_result_row.ds_collapse_flag")
lists.forEach(function(bar){
addflag(bar,"clear: left;")
})
}
}
});
}
})
});
observer.observe(search_results, {childList: true, subtree: true});
}
function init(){
let setting_btn = document.createElement('span');
setting_btn.id = "setting_btn"
setting_btn.style = "position:relative;background:linear-gradient(to right, rgb(6 207 199 / 60%) 0%, rgb(33 105 106 / 60%) 100%)"
setting_btn.innerHTML = `<a href="javascript:void(0)" style = "color:#06cfb5">家庭游戏标记 脚本设置</a></span>`
setting_btn.onclick = btnonclick
plug();
function plug(){
let headding = document.getElementById('global_action_menu')
if(headding){
headding.insertBefore(setting_btn, headding.firstChild);
}else{
setTimeout(plug,200)
}
}
function btnonclick(){
let innerHTML = `<div id="family_tool_options">
<div style="margin-bottom:6px;">目前你的家庭【${saves.familyInfo.family_name}】一共记录了 ${saves.familyGameList.GameList.length} 个共享游戏。</div>
<div style="margin-bottom:6px;">上次扫描时间: ${timestampToTime(saves.lastupDateTime)}</div>
<div style="margin-bottom:6px;"><input class="price_option_input" style="background-color: black;color: white;border: transparent;" type="checkbox" id="isAutoScan" ${saves.settings.isAutoScan ? 'checked=""':''}> 每隔24小时自动后台扫描并缓存 </div>
</div>
`
ShowConfirmDialog(`脚本设置`,innerHTML,'扫描家庭库','取消','清空库记录',{strSubTitle:'点击外部空白处即可保存退出',bExplicitDismissalOnly:false}).done(function(arg){
if(arg == 'SECONDARY'){
ShowConfirmDialog('再次确认','你即将清空当前保存的家庭库列表,该行为无法撤销!','好的','算了').done(() =>{
saves = readstorage(true)
savestorage()
ShowAlertDialog('完成','已经清除所有的缓存','好的')
})
}else{
scan(true)
}
}).fail(()=>{
saves.settings.isAutoScan = isAutoScan.checked
savestorage()
})
}
}
function observer_4(){
let block = document.getElementById('search_result_container')
if(block){
let lists = block.querySelectorAll("a.search_result_row.ds_collapse_flag")
lists.forEach(function(bar){
addflag(bar,"clear: left;")
})
}else{
setTimeout(observer_4,200)
}
}
function observer_3(){
let block = document.querySelector('div.home_tabs_content')
if(block){
let lists = block.querySelectorAll("a.tab_item")
lists.forEach(function(bar){
addflag(bar,"clear: both;")
})
block = document.querySelector('div.carousel_container.maincap')
lists = block.querySelectorAll("a.store_main_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.querySelector('div.carousel_container.spotlight')
lists = block.querySelectorAll("div.home_area_spotlight")
lists.forEach(function(bar){
addflag(bar)
})
lists = block.querySelectorAll("a.store_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.getElementById('module_deep_dive')
lists = block.querySelectorAll("a.store_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.getElementById('module_recommender')
lists = block.querySelectorAll("a.store_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.getElementById('recommended_creators_carousel')
lists = block.querySelectorAll("a.store_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.querySelector('div.specials_under10_content')
lists = block.querySelectorAll("a.store_capsule")
lists.forEach(function(bar){
addflag(bar)
})
block = document.querySelector('div.marketingmessage_area')
lists = block.querySelectorAll("a.home_marketing_message")
lists.forEach(function(bar){
addflag(bar)
})
}else{
setTimeout(observer_3,200)
}
}
function observer_2(){
let block = document.querySelector('div.block.game_media_and_summary_ctn')
if(block){
appid = Number(url.split('/')[2])
if(saves.familyGameList.GameList.includes(appid)){
addBanner(block,appid)
}
}else{
setTimeout(observer_2,200)
}
}
function addflag(node,insertBeforeStyle){
if(node.querySelector("div.ds_owned_flag")) return;
let thisappid = node.getAttribute('data-ds-appid')
let thisurl = node.getAttribute('href')
if(thisappid && (thisurl == null || thisurl.startsWith('https://store.steampowered.com/app/')) && saves.familyGameList.GameList.includes(Number(thisappid))){
if(url.startsWith('/app/')){
node.classList.add('ds_owned');
}
node.classList.add('ds_flagged');
node.classList.remove('ds_wishlist')
var flag = document.createElement('div');
flag.className = "ds_flag ds_owned_flag"
flag.innerHTML = '在家庭库中 '
flag.style = "background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAKCAYAAABi8KSDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUNDNzBFNTUyMUM0MTFFNDk1REVFODRBNUU5RjA2MUYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUNDNzBFNTYyMUM0MTFFNDk1REVFODRBNUU5RjA2MUYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5Q0M3MEU1MzIxQzQxMUU0OTVERUU4NEE1RTlGMDYxRiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5Q0M3MEU1NDIxQzQxMUU0OTVERUU4NEE1RTlGMDYxRiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv3vUKAAAAAlSURBVHjaYvz//z8DsYARpFhISAivjnfv3jGSp3jUGeQ4AyDAADZHNe2nyOBrAAAAAElFTkSuQmCC') no-repeat 4px 4px #06cfbe"
if(insertBeforeStyle){
node.insertBefore(flag, node.querySelector(`[style*="${insertBeforeStyle}"]`).nextSibling);
}else{
node.appendChild(flag);
}
node.querySelectorAll("div.ds_flag.ds_wishlist_flag").forEach((wishlist_flag)=>{wishlist_flag.remove()})
}
}
function addBanner(block,appid){
let appname = appHubAppName.innerText
let owned = false
let thisgameInfo = saves.familyGameList.GameInfo[appid]
if(block.querySelector('div.game_area_already_owned.page_content')|| thisgameInfo.owners.includes(steamid)){
owned = true
}
if(owned == false){
var headplug = document.createElement('div');
var targetElement = block.querySelector('div.queue_overflow_ctn');
headplug.style = "background:linear-gradient(to right, rgb(6 207 199 / 60%) 0%, rgb(33 105 106 / 60%) 100%);color:#06cfb5"
headplug.className = "game_area_already_owned page_content"
headplug.innerHTML =`<div class="game_area_already_owned_ctn" >
<div class="ds_owned_flag ds_flag" style="background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAKCAYAAABi8KSDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUNDNzBFNTUyMUM0MTFFNDk1REVFODRBNUU5RjA2MUYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUNDNzBFNTYyMUM0MTFFNDk1REVFODRBNUU5RjA2MUYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5Q0M3MEU1MzIxQzQxMUU0OTVERUU4NEE1RTlGMDYxRiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5Q0M3MEU1NDIxQzQxMUU0OTVERUU4NEE1RTlGMDYxRiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv3vUKAAAAAlSURBVHjaYvz//z8DsYARpFhISAivjnfv3jGSp3jUGeQ4AyDAADZHNe2nyOBrAAAAAElFTkSuQmCC) no-repeat 4px 4px #06cfbe">在家庭库中 </div>
<div class="already_in_library" >您的 Steam 家庭库中已有《${appname}》</div>
</div>`
targetElement.parentNode.insertBefore(headplug, targetElement.nextSibling);
var endplug = document.createElement('div');
targetElement = block.querySelector('div.purchase_options_content');
endplug.className = "game_area_play_stats"
endplug.innerHTML = `<div class="already_owned_actions">
<div class="game_area_already_owned_btn">
<a class="btnv6_lightblue_blue btnv6_border_2px btn_medium" href="https://store.steampowered.com/about/?snr=1_5_9__owned-game">
<span>安装 Steam</span>
</a>
</div>
<div class="game_area_already_owned_btn">
<a class="btnv6_lightblue_blue btnv6_border_2px btn_medium" href="steam://launch/${appid}/Dialog">
<span>马上开玩</span>
</a>
</div>
<div id ="see_family_benefactor" style="position: relative; display: inline-block;" data-tooltip-text="您有 ${thisgameInfo.owners.length} 个家庭组成员拥有此游戏"><div class="game_area_already_owned_btn">
<a class="btnv6_lightblue_blue btnv6_border_2px btn_medium">
<span>查看贡献者</span>
</a>
<div style="position: absolute; top: -5px; right: -8px; background-color: red; color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; justify-content: center; align-items: center;">
<span style="font-size: 14px;">${thisgameInfo.owners.length}</span>
</div>
</div></div>
</div>
<div style="clear:left;"></div>`
targetElement.parentNode.insertBefore(endplug, targetElement);
(function observer_1(){
let btn = document.getElementById('see_family_benefactor')
if(btn){
let innerHTML = `<div style='margin-bottom:6px;'>您有 ${thisgameInfo.owners.length} 个家庭组成员拥有此游戏:</div>`
thisgameInfo.owners.forEach((steamid)=>{
innerHTML+= `<div style='margin-bottom:6px;'>${saves.familyInfo.steamIdtoName[steamid]}</div>`
})
innerHTML+= `<div style='margin-bottom:6px;'>--------------------------------------------</div>
<div style='margin-bottom:6px;'>该游戏最早由【${saves.familyInfo.steamIdtoName[thisgameInfo.owners[0]]}】于 ${timestampToTime(thisgameInfo.time)} 购入。</div>`
btn.onclick = function(){
ShowAlertDialog(`【${saves.familyInfo.family_name}】游戏贡献者`,innerHTML,'好的')
}
}else{
setTimeout(observer_1,200)
}
})();
}
}
function getGameAppid(element){
return Number(element.firstChild.firstChild.getAttribute('src').split('/')[5])
}
function getGameCounts(containGames_panel){
return Number(containGames_panel.querySelector('div.LP9H7bBiPB8N8jFzCQumL').lastChild.innerText.match(/\d*/)[0])
}
})();
function scan(isdialog){
ShowAlertDialog('提示','即将开始扫描,请确认已加入一个有效的家庭组,否则脚本可能会出错,扫描期间不要关闭浏览器,耐心等待!','好的,开始扫描').done(()=>{
if(isdialog){
dialog = ShowBlockingWaitDialog('正在扫描家庭组库存...')
}
getfamilyInfo(access_token).then((returnjson) => {
saves.familyInfo = returnjson
savestorage()
getfamilyGameList(access_token,saves.familyInfo.family_groupid).then((returnjson) => {
saves.familyGameList = returnjson
saves.lastupDateTime = g_ServerTime
savestorage()
if(isdialog) dialog.Dismiss()
ShowAlertDialog('完成',`已将${saves.familyGameList.GameList.length}个家庭库游戏记录到本地缓存。`,'好的')
})
})
})
}
function getfamilyGameList(access_token,family_groupid){
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
var json = null
var returnjson = {"GameList":[],"GameInfo":{}}
xhr.open("GET", `https://api.steampowered.com/IFamilyGroupsService/GetSharedLibraryApps/v1/?access_token=${access_token}&family_groupid=${family_groupid}&include_own=true&include_excluded=false&include_non_games=false`, true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
json = JSON.parse(xhr.responseText).response;
if(json){
json.apps.forEach((app)=>{
if(app.exclude_reason == 0){
returnjson.GameList.push(app.appid)
returnjson.GameInfo[app.appid] = {"owners":app.owner_steamids,
"time":app.rt_time_acquired}
}
})
resolve(returnjson)
}
} else {
console.error("请求出错:", xhr.statusText);
}
};
xhr.send();
});
}
function getfamilyInfo(access_token){
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
var json
xhr.open("GET", `https://api.steampowered.com/IFamilyGroupsService/GetFamilyGroupForUser/v1/?access_token=${access_token}&include_family_group_response=true`, true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
json = JSON.parse(xhr.responseText).response;
if(json){
var returnjson = {
"family_groupid":json.family_groupid,
"family_name":json.family_group.name,
"family_member":json.family_group.members,
"steamIdtoName":{}
}
getUserNameBySteamId(access_token,json.family_group.members).then((ret)=>{
returnjson.family_member = ret.family_member
returnjson.steamIdtoName = ret.steamIdtoName
resolve(returnjson)
})
}
} else {
reject("请求出错:", xhr.statusText);
}
};
xhr.send();
})
}
function getUserNameBySteamId(access_token,family_member) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
var json = null
var steamIdtoName ={}
var url = `https://api.steampowered.com/IPlayerService/GetPlayerLinkDetails/v1/?access_token=${access_token}`
let i = 0
family_member.forEach((member)=>{
url+=`&steamids[${i}]=${member.steamid}`
i++
})
xhr.open("GET",url , true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
json = JSON.parse(xhr.responseText).response;
if(json){
json.accounts.forEach((user)=>{
let i = 0
family_member.forEach((member)=>{
if(member.steamid == user.public_data.steamid){
family_member[i].userName = user.public_data.persona_name
}
i++
})
steamIdtoName[user.public_data.steamid]=user.public_data.persona_name
})
resolve({family_member:family_member,steamIdtoName:steamIdtoName})
}
} else {
console.error("请求出错:", xhr.statusText);
}
};
xhr.send();
});
}
function readstorage(isnew){
var saves = GM_getValue('saves')
let newsaves = {
version : 20240407,
familyGameList:{"GameList":[],"GameInfo":{}},
familyInfo:{"family_groupid":null,
"family_name":null,
"family_member":{},
"steamIdtoName":{}},
lastupDateTime:0,
settings:{isAutoScan:true}
}
if(isnew) return newsaves
if(saves){
if(saves.version == newsaves.version){
return saves
}else{
isupdate=true
ShowConfirmDialog('脚本提示','脚本缓存列表结构升级,缓存的家庭库列表需要重新扫描!','扫描家庭库','取消').done(()=>{scan(true)}).fail(()=>{
ShowAlertDialog('脚本提示','如果需要手动扫描,可以在Steam主页右上角进入进行扫描','好的')
})
//newsaves.familyGameList.GameList = saves.familyGameList
//newsaves.lastupDateTime = saves.lastupDateTime//存档结构升级,兼容旧版
}
}
return newsaves
}
function savestorage(){
GM_setValue('saves',saves)
}
function timestampToTime(timestamp) {
if(timestamp == 0){return '无记录'}
timestamp = timestamp ? timestamp : null;
timestamp *= 1000
let date = new Date(timestamp);//时间戳为10位需*1000,时间戳为13位的话不需乘1000
let Y = date.getFullYear() + '-';
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
let D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ';
let h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':';
let m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':';
let s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
return Y + M + D + h + m + s;
}