NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name HumbleBundle Key Exporter
// @namespace Violentmonkey Scripts
// @match https://www.humblebundle.com/home/keys*
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2/dist/solid.min.js
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/ui@0.7
// @require https://unpkg.com/lz-string@1.5.0/libs/lz-string.js
// @require https://cdn.datatables.net/v/dt/dt-2.3.0/b-3.2.3/b-colvis-3.2.3/date-1.5.5/sb-1.8.2/datatables.min.js
// @resource DATATABLES_CSS https://cdn.datatables.net/v/dt/dt-2.3.0/b-3.2.3/b-colvis-3.2.3/date-1.5.5/sb-1.8.2/datatables.min.css
// @version 0.3.0
// @author MrMarble
// @license MIT
// @description Userscript that aids in exporting Humble Bundle keys from the Humble Bundle website.
// @icon https://www.google.com/s2/favicons?domain=humblebundle.com&sz=32
// @downloadURL https://github.com/MrMarble/hb-key-exporter/releases/latest/download/hb-key-exporter.user.js
// @homepageURL https://github.com/MrMarble/hb-key-exporter
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function (web, solidJs, LZString, DataTable, dom, ui) {
'use strict';
const getCategory = category => {
switch (category) {
case 'storefront':
return 'Store';
case 'bundle':
return 'Bundle';
case 'subscriptioncontent':
return 'Choice';
default:
return 'Other';
}
};
const loadOrders = () => Object.keys(localStorage).filter(key => key.startsWith('v2|')).map(key => JSON.parse(LZString.decompressFromUTF16(localStorage.getItem(key)))).filter(order => {
var _order$tpkd_dict;
return order == null || (_order$tpkd_dict = order.tpkd_dict) == null || (_order$tpkd_dict = _order$tpkd_dict.all_tpks) == null ? void 0 : _order$tpkd_dict.length;
});
const getProducts = (orders, ownedApps) => orders.flatMap(order => order.tpkd_dict.all_tpks.map(product => ({
machine_name: product.machine_name || '-',
category: getCategory(order.product.category),
category_id: order.gamekey,
category_human_name: order.product.human_name || '-',
human_name: product.human_name || product.machine_name || '-',
key_type: product.key_type || '-',
type: product.is_gift ? 'Gift' : product.redeemed_key_val ? 'Key' : '-',
redeemed_key_val: product.redeemed_key_val || '',
is_gift: product.is_gift || false,
is_expired: product.is_expired || false,
expiry_date: product.expiry_date || '',
steam_app_id: product.steam_app_id,
created: order.created || '',
keyindex: product.keyindex,
owned: product.steam_app_id ? ownedApps.includes(product.steam_app_id) ? 'Yes' : 'No' : '-'
})));
const redeem = async (product, gift = false) => {
console.log('Redeeming product:', product.machine_name);
const data = await fetch('https://www.humblebundle.com/humbler/redeemkey', {
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: `keytype=${product.machine_name}&key=${product.category_id}&keyindex=${product.keyindex}${gift ? '&gift=true' : ''}`,
method: 'POST',
mode: 'cors'
}).then(res => res.json());
console.log('Redeem response:', data);
return gift ? `https://www.humblebundle.com/gift?key=${data.giftkey}` : data.key;
};
const fetchOwnedApps = async () => new Promise(resolve => GM_xmlhttpRequest({
url: 'https://store.steampowered.com/dynamicstore/userdata',
method: 'GET',
timeout: 5000,
responseType: 'json',
onload: resolve
})).then(data => {
var _data$response, _data$response2;
return ((data == null || (_data$response = data.response) == null ? void 0 : _data$response.rgOwnedPackages) || []).concat((data == null || (_data$response2 = data.response) == null ? void 0 : _data$response2.rgOwnedApps) || []);
}).catch(() => []);
let ownedApps = [];
const loadOwnedApps = async (refresh = false) => {
if (!refresh && ownedApps.length) {
console.debug('Using cached owned apps');
return ownedApps;
}
console.debug('Fetching owned apps from Steam');
// Try to load from localStorage first
const storedApps = localStorage.getItem('hb-key-exporter-ownedApps');
if (storedApps) {
return JSON.parse(LZString.decompressFromUTF16(storedApps));
}
// If not found, fetch from Steam
ownedApps = await fetchOwnedApps();
if (!ownedApps) {
return [];
}
// Store the result in localStorage for future use
localStorage.setItem('hb-key-exporter-ownedApps', LZString.compressToUTF16(JSON.stringify(ownedApps)));
return ownedApps;
};
var styles = {"platform":"style-module_platform__kpbfk","expired":"style-module_expired__IvOyt","select":"style-module_select__WN4g9","actions":"style-module_actions__aALan","btn":"style-module_btn__Bc4TY","row_actions":"style-module_row_actions__dEszH"};
var stylesheet="td.style-module_platform__kpbfk{font-size:20px;text-align:center}tr.style-module_expired__IvOyt{background-color:#f8d7da!important}.style-module_select__WN4g9{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#f1f3f6 url(//cdn.humblebundle.com/static/hashed/ce14e404160fcb5d989503e532c4324f0297014d.gif) no-repeat 90% 50%;border:1px solid #ccc;padding:5px 30px 5px 10px;position:relative;top:-1px}.style-module_actions__aALan{align-items:center;display:flex;gap:10px;justify-content:end}.style-module_btn__Bc4TY{background-color:#e9e9ed;border:1px solid #8f8f9d;border-radius:5px;padding:1px 4px}.style-module_row_actions__dEszH{display:flex;gap:2px}";
var _tmpl$$3 = /*#__PURE__*/web.template(`<table id=hb_extractor-table class="display compact">`);
function Table({
products,
setDt
}) {
let tableRef;
solidJs.onMount(() => {
console.debug('Mounting table with', products.length, 'products');
setDt(() => new DataTable(tableRef, {
columnDefs: [{
targets: [7, 8],
render: DataTable.render.date()
}, {
targets: [9],
data: null,
defaultContent: ''
}],
order: {
idx: 7,
dir: 'desc'
},
columns: [{
title: 'Type',
data: 'key_type',
type: 'html-utf8',
render: (data, type, row) => dom.hm('i', {
class: `hb hb-key hb-${data}`,
onclick: () => ui.showToast(JSON.stringify(row, null, 2))
}, dom.hm('span', {
class: 'hidden',
innerText: data
})),
className: styles.platform
}, {
title: 'Name',
data: 'human_name',
type: 'html-utf8',
render: (data, _, row) => row.steam_app_id ? dom.hm('a', {
href: `https://store.steampowered.com/app/${row.steam_app_id}`,
target: '_blank',
innerText: data
}) : data
}, {
title: 'Category',
data: 'category',
type: 'string-utf8'
}, {
title: 'Bundle Name',
data: 'category_human_name',
type: 'html-utf8',
render: (data, _, row) => dom.hm('a', {
href: `https://www.humblebundle.com/download?key=${row.category_id}`,
target: '_blank',
innerText: data
})
}, {
title: 'Gift',
data: 'type',
type: 'string-utf8'
}, {
title: 'Revealed',
data: row => row.is_gift || row.redeemed_key_val ? 'Yes' : 'No',
type: 'string-utf8'
}, {
title: 'Owned',
data: 'owned',
type: 'string-utf8'
}, {
title: 'Purchased',
data: 'created',
type: 'date'
}, {
title: 'Exp. Date',
data: 'expiry_date',
type: 'date'
}, {
title: '',
orderable: false,
searchable: false,
data: row => {
const actions = [];
if (row.redeemed_key_val) {
actions.push(dom.hm('button', {
class: styles.btn,
title: 'Copy to clipboard',
type: 'button',
onclick: () => {
navigator.clipboard.writeText(row.redeemed_key_val);
ui.showToast('Copied to clipboard');
}
}, dom.hm('i', {
class: 'hb hb-key hb-clipboard'
})));
}
if (row.redeemed_key_val && !row.is_gift && !row.is_expired && row.key_type === 'steam') {
actions.push(dom.hm('a', {
class: styles.btn,
href: `https://store.steampowered.com/account/registerkey?key=${row.redeemed_key_val}`,
target: '_blank'
}, dom.hm('i', {
class: 'hb hb-shopping-cart-light',
title: 'Redeem'
})));
}
if (row.redeemed_key_val && row.is_gift && !row.is_expired) {
actions.push(dom.hm('a', {
class: styles.btn,
href: row.redeemed_key_val,
target: '_blank'
}, dom.hm('i', {
class: 'hb hb-shopping-cart-light',
title: 'Redeem'
})));
}
if (!row.redeemed_key_val && !row.is_gift && !row.is_expired) {
actions.push(dom.hm('button', {
class: styles.btn,
type: 'button',
onclick: () => {
redeem(row).then(data => navigator.clipboard.writeText(data)).then(() => ui.showToast('Key copied to clipboard'));
}
}, dom.hm('i', {
class: 'hb hb-magic',
title: 'Reveal'
})), dom.hm('button', {
class: styles.btn,
type: 'button',
onclick: () => {
redeem(row, true).then(link => navigator.clipboard.writeText(link)).then(() => ui.showToast('Link copied to clipboard'));
}
}, dom.hm('i', {
class: 'hb hb-gift',
title: 'Create gift link'
})));
}
return dom.hm('div', {
class: styles.row_actions
}, actions);
}
}],
data: products,
layout: {
top1: 'searchBuilder'
},
createdRow: function (row, data) {
if (data.is_expired) {
row.classList.add(styles.expired);
}
}
}));
});
console.debug('Table Loaded');
return (() => {
var _el$ = _tmpl$$3();
var _ref$ = tableRef;
typeof _ref$ === "function" ? web.use(_ref$, _el$) : tableRef = _el$;
return _el$;
})();
}
var _tmpl$$2 = /*#__PURE__*/web.template(`<button type=button title="Reload products"><i class="hb hb-refresh">`);
function Refresh({
refresh
}) {
return (() => {
var _el$ = _tmpl$$2();
_el$.$$click = () => refresh();
return _el$;
})();
}
web.delegateEvents(["click"]);
var _tmpl$$1 = /*#__PURE__*/web.template(`<div><label for=claim><input type=checkbox id=claim name=claim>Claim unredeemed games</label><select name=claimType id=claimType><option value disabled>What to claim</option><option value=key selected>Key</option><option value=gift>Gift link</option></select><label for=filtered><input type=checkbox id=filtered name=filtered>Use table filter</label><select name=export id=export><option value disabled selected>Export format</option><option value=asf>ASF</option><option value=keys>Keys</option><option value=csv>CSV</option></select><button type=button class=primary-button>`),
_tmpl$2$1 = /*#__PURE__*/web.template(`<i class="hb hb-spin hb-spinner">`);
function Actions({
dt
}) {
const [exportType, setExportType] = solidJs.createSignal('');
const [filtered, setFiltered] = solidJs.createSignal(false);
const [claim, setClaim] = solidJs.createSignal(false);
const [claimType, setClaimType] = solidJs.createSignal('key');
const [exporting, setExporting] = solidJs.createSignal(false);
const exportASF = products => {
const keys = products.filter(product => !product.is_gift && product.redeemed_key_val && !product.is_expired && product.key_type === 'steam').map(product => `${product.redeemed_key_val}\t${product.human_name}`).join('\n');
navigator.clipboard.writeText(keys);
};
const exportKeys = products => {
const keys = products.filter(product => !product.is_gift && product.redeemed_key_val).map(product => product.redeemed_key_val).join('\n');
navigator.clipboard.writeText(keys);
};
const exportCSV = products => {
const header = Object.keys(products[0]);
const csv = products.map(product => {
return header.map(h => product[h]).join(',');
}).join('\n');
navigator.clipboard.writeText(header + '\n' + csv);
};
const exportToClipboard = async () => {
setExporting(true);
const toExport = dt().rows({
search: filtered() ? 'applied' : 'none'
}).data().toArray();
if (claim()) {
for (const product of toExport) {
if (product.redeemed_key_val) {
continue;
}
try {
product.redeemed_key_val = await redeem(product, claimType() === 'gift');
} catch (e) {
console.error('Error redeeming product:', product.machine_name, e);
}
}
}
switch (exportType()) {
case 'asf':
exportASF(toExport);
break;
case 'keys':
exportKeys(toExport);
break;
case 'csv':
exportCSV(toExport);
break;
}
setExporting(false);
ui.showToast('Exported to clipboard');
};
return (() => {
var _el$ = _tmpl$$1(),
_el$2 = _el$.firstChild,
_el$3 = _el$2.firstChild,
_el$4 = _el$2.nextSibling,
_el$5 = _el$4.nextSibling,
_el$6 = _el$5.firstChild,
_el$7 = _el$5.nextSibling,
_el$8 = _el$7.nextSibling;
_el$3.addEventListener("change", e => setClaim(e.target.checked));
_el$4.addEventListener("change", e => setClaimType(e.target.value));
_el$6.addEventListener("change", e => setFiltered(e.target.checked));
_el$7.addEventListener("change", e => setExportType(e.target.value));
_el$8.$$click = exportToClipboard;
web.insert(_el$8, (() => {
var _c$ = web.memo(() => !!exporting());
return () => _c$() ? _tmpl$2$1() : 'Export';
})());
web.effect(_p$ => {
var _v$ = styles.actions,
_v$2 = styles.select,
_v$3 = !claim(),
_v$4 = styles.select,
_v$5 = !exportType() || exporting();
_v$ !== _p$.e && web.className(_el$, _p$.e = _v$);
_v$2 !== _p$.t && web.className(_el$4, _p$.t = _v$2);
_v$3 !== _p$.a && _el$4.classList.toggle("hidden", _p$.a = _v$3);
_v$4 !== _p$.o && web.className(_el$7, _p$.o = _v$4);
_v$5 !== _p$.i && (_el$8.disabled = _p$.i = _v$5);
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined
});
return _el$;
})();
}
web.delegateEvents(["click"]);
var _tmpl$ = /*#__PURE__*/web.template(`<details><summary><h3><i class="hb hb-key"></i> Advanced Exporter</h3></summary><div>`),
_tmpl$2 = /*#__PURE__*/web.template(`<p>Loading products...`);
function App() {
const [products, {
refetch: refresh
}] = solidJs.createResource(async (_, info) => {
console.debug('Loading products...');
const orders = loadOrders();
const owned = await loadOwnedApps(info.refetching);
console.debug('Loaded', orders.length, 'orders,', owned.length, 'owned apps');
return getProducts(orders, owned);
});
const [dt, setDt] = solidJs.createSignal(null);
console.debug('App loaded');
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$3 = _el$2.nextSibling;
_el$3.style.setProperty("display", "flex");
_el$3.style.setProperty("justify-content", "end");
_el$3.style.setProperty("align-items", "center");
web.insert(_el$3, web.createComponent(Refresh, {
refresh: refresh
}));
web.insert(_el$, web.createComponent(solidJs.Show, {
get when() {
var _products;
return (_products = products()) == null ? void 0 : _products.length;
},
get fallback() {
return _tmpl$2();
},
get children() {
return web.createComponent(Table, {
get products() {
return products();
},
setDt: setDt
});
}
}), null);
web.insert(_el$, web.createComponent(Actions, {
dt: dt
}), null);
return _el$;
})();
}
var css_248z = ".base-main-wrapper,.inner-main-wrapper{width:1020px!important}#hb_extractor-container{margin-bottom:15px}#hb_extractor-container details{display:unset;width:100%}#hb_extractor-container .dtsb-titleRow{display:none}";
// Create a container for the script
document.querySelector('.inner-main-wrapper').insertAdjacentHTML('afterbegin', '<div id="hb_extractor-container"></div>');
// Render the app
web.render(App, document.getElementById('hb_extractor-container'));
// Add CSS styles
GM_addStyle(GM_getResourceText('DATATABLES_CSS'));
GM_addStyle(stylesheet);
GM_addStyle(css_248z);
})(VM.solid.web, VM.solid, LZString, DataTable, VM, VM);