// ==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 // @downloadURL none // ==/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 = `家庭游戏标记 脚本设置` 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 = `
目前你的家庭【${saves.familyInfo.family_name}】一共记录了 ${saves.familyGameList.GameList.length} 个共享游戏。
上次扫描时间: ${timestampToTime(saves.lastupDateTime)}
  每隔24小时自动后台扫描并缓存 
` 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 =`
在家庭库中  
您的 Steam 家庭库中已有《${appname}》
` 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 = `
安装 Steam
马上开玩
查看贡献者
${thisgameInfo.owners.length}
` targetElement.parentNode.insertBefore(endplug, targetElement); (function observer_1(){ let btn = document.getElementById('see_family_benefactor') if(btn){ let innerHTML = `
您有 ${thisgameInfo.owners.length} 个家庭组成员拥有此游戏:
` thisgameInfo.owners.forEach((steamid)=>{ innerHTML+= `
${saves.familyInfo.steamIdtoName[steamid]}
` }) innerHTML+= `
--------------------------------------------
该游戏最早由【${saves.familyInfo.steamIdtoName[thisgameInfo.owners[0]]}】于 ${timestampToTime(thisgameInfo.time)} 购入。
` 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; }