您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-exports Steam games list with WebSocket login automation support
// ==UserScript== // @name Steam Games Export with WebSocket // @namespace steamutils // @version 0.7.4 // @description Auto-exports Steam games list with WebSocket login automation support // @author mustafachyi // @match *://steamcommunity.com/* // @grant GM_cookie // @grant GM_xmlhttpRequest // @grant GM_download // @connect steamcommunity.com // @connect localhost // @connect 127.0.0.1 // @run-at document-start // ==/UserScript== (() => { 'use strict'; // Configuration const CONFIG = { urls: { base: 'https://steamcommunity.com', login: '/login/home/', games: '/games' }, storage: { profiles: 'steam_exported_profiles', username: 'steam_last_username', mode: 'steam_export_mode' }, retry: { max: 20, delay: 500, loginCheck: 1500 }, ui: { notifyDuration: 3000, animDuration: 300 }, keys: { logout: { ctrl: true, alt: true, key: 'l' } }, ws: { url: 'ws://127.0.0.1:27060', fallback: true } }; // URL utilities const url = { isLogin: () => location.href.includes('/login/home'), isGames: () => location.pathname.includes('/games'), isProfile: () => /\/(?:id|profiles)\/[^\/]+(?:\/home|\/?$)/.test(location.pathname), isFamilyPin: () => location.href.includes('/my/goto'), getBase: () => (location.href.match(/(.*\/(?:id|profiles)\/[^\/]+)(?:\/home)?/) || [])[1] || null, getSteamId: () => { const match = location.pathname.match(/\/(?:id|profiles)\/([^\/]+)(?:\/home)?/); return match ? match[1] : null; }, resolveVanityURL: async (vanityURL) => { return utils.request(`https://steamcommunity.com/id/${vanityURL}?xml=1`, { parser: res => { const steamID64 = res.responseText.match(/<steamID64>(\d+)<\/steamID64>/); return steamID64 ? steamID64[1] : null; } }); } }; // Early URL handling if (location.href === `${CONFIG.urls.base}/` || location.href === CONFIG.urls.base) { location.replace(`${CONFIG.urls.base}${CONFIG.urls.login}`); return; } const profileMatch = location.pathname.match(/^\/(id|profiles)\/([^\/]+)(?:\/(?:home)?)?$/); if (profileMatch) { const [, type, id] = profileMatch; try { const profiles = JSON.parse(localStorage.getItem(CONFIG.storage.profiles) || '{}'); if (profiles[id] === undefined) { location.replace(`${CONFIG.urls.base}/${type}/${id}${CONFIG.urls.games}`); return; } } catch {} } // Utilities const utils = { setReactValue(input, value) { Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(input, value); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }, async handleLogin(loginButton) { const [userInput, passInput] = document.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx'); if (userInput && passInput && loginButton) { try { loginButton.click(); setTimeout(() => { if (url.isLogin()) ws.send({ type: 'login_failed' }); }, CONFIG.retry.loginCheck); return true; } catch (e) { console.log('Login error:', e); } } const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f'); if (form) { try { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); setTimeout(() => { if (url.isLogin()) ws.send({ type: 'login_failed' }); }, CONFIG.retry.loginCheck); return true; } catch (e) { console.log('Form submission error:', e); } } return false; }, request: (url, { method = 'GET', parser } = {}) => new Promise(resolve => GM_xmlhttpRequest({ method, url, onload: res => resolve(parser ? parser(res) : res), onerror: () => resolve(null) }) ) }; // WebSocket handler const ws = { conn: null, connected: false, mode: localStorage.getItem(CONFIG.storage.mode) || 'manual', connect() { if (this.conn?.readyState <= WebSocket.OPEN) return; try { this.conn = new WebSocket(CONFIG.ws.url); this.conn.onopen = () => { this.send({ type: 'identify', client: 'userscript', version: '0.7.4' }); this.connected = true; }; this.conn.onmessage = ({ data }) => { try { const msg = JSON.parse(data); const handler = this.handlers[msg.type]; handler && handler(msg); } catch {} }; this.conn.onclose = this.conn.onerror = () => { this.cleanup(); if (CONFIG.ws.fallback && url.isLogin()) this.fallback = true; }; } catch { this.cleanup(); if (CONFIG.ws.fallback && url.isLogin()) this.fallback = true; } }, handlers: { connected(msg) { if (ws.mode === msg.mode) return; ws.mode = msg.mode; localStorage.setItem(CONFIG.storage.mode, ws.mode); ui.notify('Connected', `Server connected in ${ws.mode} mode`); }, manual_mode() { if (ws.mode === 'manual') return; ws.mode = 'manual'; localStorage.setItem(CONFIG.storage.mode, 'manual'); ui.notify('Mode Changed', 'Switched to manual mode'); }, account_data(msg) { const [user, pass] = msg.credentials.split(':').map(s => s.trim()); auth.fillCredentials(user, pass); }, all_done() { ui.notify('Complete', 'All accounts have been processed'); } }, send(data) { return this.conn?.readyState === WebSocket.OPEN && this.conn.send(JSON.stringify(data)); }, cleanup() { if (this.conn) { this.conn.close(); this.conn = null; this.connected = false; } } }; // UI components const ui = { styles: ` .steam_export_notification{position:fixed;bottom:20px;right:20px;background:#1b2838;border:1px solid #66c0f4;color:#fff;padding:15px;border-radius:3px;box-shadow:0 0 10px rgba(0,0,0,.5);z-index:9999;font-family:"Motiva Sans",Arial,sans-serif;animation:steamNotificationSlide .3s ease-out;display:flex;align-items:center;gap:10px;min-width:280px} .steam_export_notification .icon{width:24px;height:24px;background:#66c0f4;border-radius:3px;display:flex;align-items:center;justify-content:center} .steam_export_notification .content{flex-grow:1} .steam_export_notification .title{font-weight:700;margin-bottom:3px;color:#66c0f4} .steam_export_notification .message{font-size:12px;color:#acb2b8} @keyframes steamNotificationSlide{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}} .steam_export_btn{display:inline-flex;align-items:center;padding:0 15px;line-height:24px;border-radius:2px;background:#101822;color:#fff;margin-left:10px;cursor:pointer;border:none;font-family:"Motiva Sans",Arial,sans-serif;transition:all .25s ease} .steam_export_btn:hover{background:#4e92b9} body.login .responsive_page_frame{height:100vh!important;overflow:hidden!important;display:flex!important;flex-direction:column!important} body.login .responsive_page_content{flex:1!important;overflow:hidden!important;display:flex!important;flex-direction:column!important} body.login .responsive_page_template_content{flex:1!important;display:flex!important;flex-direction:column!important;justify-content:center!important;align-items:center!important;min-height:0!important} body.login .page_content{margin:0!important;padding:0 16px!important;width:100%!important;max-width:740px!important;box-sizing:border-box!important} body.login #footer,body.login #global_header,body.login .responsive_header{position:relative!important} body.login #footer{margin-top:auto!important;padding:16px 0!important} body.login #footer_spacer{display:none!important} body.login #global_header{padding:16px 0!important} body.login .responsive_header{padding:12px 0!important} body.login .login_bottom_row{margin:16px 0!important} body.login [data-featuretarget="login"]{margin:0!important;padding:16px!important;background:rgba(0,0,0,0.2)!important;border-radius:4px!important;box-shadow:0 0 10px rgba(0,0,0,0.3)!important} `.replace(/\s+/g, ' '), init() { const style = document.createElement('style'); style.textContent = this.styles; (document.head || document.documentElement).appendChild(style); }, notify(title, message) { const el = document.createElement('div'); el.className = 'steam_export_notification'; el.innerHTML = `<div class="icon">✓</div><div class="content"><div class="title">${title}</div><div class="message">${message}</div></div>`; document.body.appendChild(el); setTimeout(() => { el.style.animation = 'steamNotificationSlide 0.3s ease-in reverse'; setTimeout(() => el.remove(), CONFIG.ui.animDuration); }, CONFIG.ui.notifyDuration); }, addExportButton() { const btn = document.createElement('a'); btn.className = 'steam_export_btn'; btn.textContent = 'Export Games'; btn.onclick = () => games.exportFromConfig(); const observer = new MutationObserver((_, obs) => { const header = document.querySelector('.profile_small_header_text'); if (header) { header.appendChild(btn); obs.disconnect(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } }; // Storage management const storage = { get(key) { try { return JSON.parse(localStorage.getItem(key) || '{}'); } catch { return {}; } }, set(key, value) { localStorage.setItem(key, JSON.stringify(value)); }, markExported(steamId, count) { const profiles = this.get(CONFIG.storage.profiles); profiles[steamId] = count; this.set(CONFIG.storage.profiles, profiles); }, shouldExport(steamId, count) { const profiles = this.get(CONFIG.storage.profiles); return profiles[steamId] === undefined || profiles[steamId] !== count; } }; // Games management const games = { async waitForData(retries = 0) { const config = document.getElementById('gameslist_config')?.dataset.profileGameslist; if (!config) { if (retries < CONFIG.retry.max) { setTimeout(() => this.waitForData(retries + 1), CONFIG.retry.delay); } return; } try { const data = JSON.parse(config); if (!data.rgGames?.length) return; if (storage.shouldExport(data.strSteamId, data.rgGames.length)) { await this.export(data, true); storage.markExported(data.strSteamId, data.rgGames.length); } !document.querySelector('.steam_export_btn') && ui.addExportButton(); } catch {} }, async exportFromConfig() { const config = document.getElementById('gameslist_config')?.dataset.profileGameslist; if (!config) return; try { const data = JSON.parse(config); if (data.rgGames?.length) { await this.export(data); storage.markExported(data.strSteamId, data.rgGames.length); } } catch {} }, async export(data, isAutoExport = false) { const username = localStorage.getItem(CONFIG.storage.username) || data.strProfileName || 'unknown'; const content = data.rgGames.map(g => g.name).join('\n'); const url = URL.createObjectURL(new Blob([content], { type: 'text/plain' })); const cleanup = setTimeout(() => URL.revokeObjectURL(url), 30000); try { await new Promise((resolve, reject) => { GM_download({ url, name: `steam_games/${username}_games.txt`, saveAs: false, onload: resolve, onerror: reject }); }); ui.notify('Games List Exported', `Saved ${data.rgGames.length} games to steam_games/${username}_games.txt`); isAutoExport && typeof Logout === 'function' && setTimeout(Logout, 1000); } catch (error) { if (error.includes('No such file or directory')) { await new Promise(resolve => { GM_download({ url: 'data:text/plain;base64,', name: 'steam_games/.folder', saveAs: false, onload: resolve }); }); return this.export(data, isAutoExport); } ui.notify('Export Failed', 'Could not save games list. Please try again.'); } finally { clearTimeout(cleanup); URL.revokeObjectURL(url); } }, async resolveID(idOrVanity) { if (/^\d+$/.test(idOrVanity)) return idOrVanity; return await url.resolveVanityURL(idOrVanity) || idOrVanity; }, async getCount(idOrVanity) { const resolvedID = await this.resolveID(idOrVanity); const urlPath = /^\d+$/.test(resolvedID) ? `profiles/${resolvedID}` : `id/${resolvedID}`; return utils.request(`${CONFIG.urls.base}/${urlPath}/games`, { parser: res => { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const data = JSON.parse(doc.getElementById('gameslist_config')?.dataset.profileGameslist || '{}'); return data.rgGames?.length || null; } }); } }; // Authentication const auth = { setupLoginCapture() { const observer = new MutationObserver((_, obs) => { const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f'); if (!form) return; const [userInput, passInput] = form.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx'); if (!userInput || !passInput) return; const loginButton = form.querySelector('button.DjSvCZoKKfoNSmarsEcTS'); if (!loginButton) return; userInput.addEventListener('paste', e => { const text = (e.clipboardData || window.clipboardData).getData('text'); if (!text.includes(':')) return; e.preventDefault(); const [user, pass] = text.split(':').map(s => s.trim()); this.fillCredentials(user, pass); }); if (!ws.fallback) { ws.connect(); setTimeout(() => ws.send({ type: 'ready_for_login' }), 500); } loginButton.addEventListener('click', () => { const username = userInput.value.trim(); username && localStorage.setItem(CONFIG.storage.username, username); }); obs.disconnect(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); }, fillCredentials(user, pass) { if (!user || !pass) return; const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f'); if (!form) return; const [userInput, passInput] = form.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx'); const loginButton = form.querySelector('button.DjSvCZoKKfoNSmarsEcTS'); if (!userInput || !passInput || !loginButton) return; utils.setReactValue(userInput, user); utils.setReactValue(passInput, pass); localStorage.setItem(CONFIG.storage.username, user); ui.notify('Credentials Filled', 'Username and password have been entered'); ws.connected && setTimeout(() => { ws.send({ type: 'credentials_filled' }); utils.handleLogin(loginButton); }, 1000); }, async checkState() { return utils.request(`${CONFIG.urls.base}/my/`, { parser: res => !res.finalUrl.includes('/login') }); }, setupLogout() { document.addEventListener('keydown', e => { const { ctrl, alt, key } = CONFIG.keys.logout; if ((!ctrl || e.ctrlKey) && (!alt || e.altKey) && e.key.toLowerCase() === key) { e.preventDefault(); typeof Logout === 'function' && Logout(); } }, true); } }; // Login page optimization if (location.href.includes('/login/home')) { const blockStyle = document.createElement('style'); blockStyle.textContent = `#footer,#global_header,.login_bottom_row{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important;position:absolute!important;width:0!important;height:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important}`; document.documentElement.appendChild(blockStyle); const observer = new MutationObserver(mutations => { for (const { addedNodes } of mutations) { for (const node of addedNodes) { if (node.nodeType !== 1) continue; if (node.matches?.('#footer,#global_header,.login_bottom_row') && node.parentNode) { node.remove(); } if (node.querySelectorAll) { node.querySelectorAll('#footer,#global_header,.login_bottom_row').forEach(el => { if (el && el.parentNode) el.remove(); }); } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); window.addEventListener('load', () => observer.disconnect(), { once: true }); } // Initialization document.addEventListener('DOMContentLoaded', async () => { ui.init(); auth.setupLogout(); if (url.isLogin()) return auth.setupLoginCapture(); if (url.isGames()) return games.waitForData(); if (url.isFamilyPin()) { ui.notify('Family Pin Protected', 'Account is protected by family pin. Logging out...'); setTimeout(() => typeof Logout === 'function' && Logout(), 1000); return; } if (!await auth.checkState()) return (location.href = CONFIG.urls.login); if (url.isProfile()) { const steamId = url.getSteamId(); if (!steamId) return; const profiles = storage.get(CONFIG.storage.profiles); const idKey = await games.resolveID(steamId); const prevCount = profiles[idKey]; if (prevCount !== undefined) { const currCount = await games.getCount(steamId); if (!currCount || currCount === prevCount) return; } const base = url.getBase(); base && (location.href = base + CONFIG.urls.games); } }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址