// ==UserScript== // @name Backloggery interop // @namespace http://tampermonkey.net/ // @version 0.5 // @description Backloggery integration with game library websites // @author LeXofLeviafan // @include *://www.backloggery.com/games.php?* // @include *://www.backloggery.com/update.php?* // @include *://www.backloggery.com/newgame.php?* // @include *://steamcommunity.com/id//games/* // @exclude *://steamcommunity.com/id//games/* // @include *://steamcommunity.com/id//stats/*/?tab=achievements // @exclude *://steamcommunity.com/id//stats/*/?tab=achievements // @include *://store.steampowered.com/app/* // @include *://steamdb.info/app/* // @include *://steamdb.info/calculator//* // @exclude *://steamdb.info/calculator//* // @include *://astats.astats.nl/astats/User_Games.php?* // @include *://www.gog.com/account // @include *://www.humblebundle.com/home/library // @include *://www.humblebundle.com/monthly/trove // @include *://*.gamersgate.com/account/* // @require https://cdnjs.cloudflare.com/ajax/libs/mithril/1.1.6/mithril.min.js // @require https://cdn.jsdelivr.net/npm/coffeescript@2.4.1/lib/coffeescript-browser-compiler-legacy/coffeescript.js // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @downloadURL none // ==/UserScript== var inline_src = (<> x merge = (os...) -> Object.assign {}, os... fromPairs = (pairs) -> merge ([k]: v for [k, v] in pairs.filter identity)... keymap = (ks, f) -> fromPairs ([k, f k] for k in ks) objmap = (o, f) -> fromPairs ([k, f(v, k, o)] for k, v of o) pick = (o, keys...) -> fromPairs ([k, o[k]] for k in keys when k of o) method = (o, k, def=->) -> o?[k]?.bind?(o) or def setFn = (xs) -> method (new Set xs), 'has' last = (l) -> l[l.length - 1] when_ = (x, f) -> x and f(x) replace = (s, re, pattern) -> s.match(re) and s.replace(re, pattern) qstr = (s) -> if not s.includes('?') then "" else s[1 + s.indexOf '?'..] query = (s) -> fromPairs (l[1..] for l in qstr(s).split('&').map((s) -> s.match /([^=]+)=(.*)/) when l) slugify = (s) -> s.toLowerCase().replace(/[.]/g, '').replace(/[^a-z0-9+]+/g, '-').replace(/(^-*|-*$)/g, '') capitalize = (s) -> do (z = "#{s}") -> z[...1].toUpperCase() + z[1..] statStr = (o, ks...) -> ("#{capitalize k}: #{o[k]}" for k in ks when k of o).join '\n' PAGE = location.href PARAMS = query location.search RE = backloggeryUpdate: "backloggery\\.com/update\\.php" backloggeryCreate: "backloggery\\.com/newgame\\.php" backloggeryLibrary: "backloggery\\.com/games\\.php" steamLibrary: "steamcommunity\\.com/id/[^/]+/games/\\?tab=all" steamRecent: "steamcommunity\\.com/id/[^/]+/games($|/$|/\\?tab=recent)" steamAchievements: "steamcommunity\\.com/id/[^/]+/stats/[^/]+" steamDetails: "store\\.steampowered\\.com/app/([^/]+)" steamDbDetails: "steamdb\\.info/app/[^/]+" steamDbLibrary: "steamdb\\.info/calculator/[^/]+/" steamStats: "astats\\.astats\\.nl/astats/User_Games\\.php" gogLibrary: "gog\\.com/account" humbleLibrary: "humblebundle\\.com/home/library" humbleTrove: "humblebundle\\.com/monthly/trove" ggateLibrary: "gamersgate\\.com/account/(games|wishlist|achievements)" # they share a page and can switch without reload DATA = do (TROVE = objmap(GM_getValue('humble-trove', {}), (o) -> merge o, url: "https://www.humblebundle.com/monthly/trove") STATS = GM_getValue('steam-stats', {}), PLATFORMS = GM_getValue('steam-platforms', {})) -> steam: objmap GM_getValue('steam', {}), (x, k) -> merge(x, url: x.link, achievements: STATS[k] or '?', worksOn: PLATFORMS[k] or 's') gog: objmap GM_getValue('gog', {}), (x) -> merge(x, url: "https://gog.com#{x.url}", completed: if x.completed then 'yes' else 'no') humble: objmap merge(TROVE, GM_getValue 'humble'), (x, id) -> merge(TROVE[id], x) ggate: objmap GM_getValue('ggate', {}), (x, id) -> merge(x, url: "https://gamersgate.com/#{id}") OS = w: ["Windows", 'fa-windows'], l: ["Linux", 'fa-linux'], m: ["MacOS", 'fa-apple'], a: ["Android", 'fa-android'], s: ["Steam", 'fa-steam'] slugs = (o) -> fromPairs ([slugify(v.name), k] for k, v of o) $clear = (e) -> e.removeChild e.firstChild while e.firstChild; e $append = (parent, children...) -> parent.appendChild e for e in children; parent $before = (neighbour, children...) -> neighbour.parentElement.insertBefore(e, neighbour) for e in children; neighbour $after = (neighbour, children...) -> $before(neighbour.nextSibling, children...); neighbour $e = (tag, options, children...) -> $append Object.assign(document.createElement(tag), options), children... $get = (xpath, e=document) -> document.evaluate(xpath, e, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue $find = (selector, e=document) -> e.querySelector selector $find_ = (selector, e=document) -> Array.from e.querySelectorAll selector $hasClass = (e, clss) -> do (l = e.classList) -> l and clss.split(' ').every((s) -> not s or l.contains s) $visibility = (e, x) -> e.style.visibility = if x then 'visible' else 'hidden' $markUpdate = (k) -> GM_setValue 'updated', merge(GM_getValue('updated'), [k]: +new Date) $assertEq = (a, b, err) -> a is b or alert(if typeof err isnt 'function' then err else err a, b) $stop = (f=->) -> (e) -> f e; e.stopPropagation(); no $keepScroll = (e, f) -> do (x = e.scrollTop) -> f(); m.redraw(); e.scrollTop = x $query = (url) -> new Promise (resolve, reject) -> do (xhr = new XMLHttpRequest) -> xhr.open 'GET', url [xhr.onerror, xhr.onload] = [reject, -> resolve JSON.parse xhr.response] xhr.send() words = (s) -> slugify(s).split('-').sort().reverse() matching = (ss, zs) -> do (res = 0, i = 0, j = 0) -> while i < ss.length and j < zs.length [s, z] = [ss[i], zs[j]] if s is z i++; j++; res += 2 else if z.startsWith s i++; j++; res += 1 else if s < z then j++ else i++ res order = (sets, exclude, text, k) -> do (d = DATA[k], l = words(text), f = (s) -> not exclude["#{k}##{s}"]) -> o = objmap(sets[k] or {}, (ss) -> matching l, ss) Object.keys(sets[k] or {}).sort (a, b) -> f(b)-f(a) or o[b]-o[a] or d[a].name.localeCompare d[b].name $update = (k, games1) -> do (games0 = GM_getValue k, {}) -> [ids1, ids0] = [games1, games0].map Object.keys removed = (id for id in ids0 when id not of games1) added = (id for id in ids1 when id not of games0) $markUpdate k GM_setValue k, games1 setTimeout -> alert "Backloggery interop: added #{added.length} games, removed #{removed.length} games" $mergeData = (k, o) -> GM_setValue k, merge(GM_getValue(k), o) $logo = (k, id) -> do (o = DATA[k][id]) -> switch k when 'steam' then [o.logo, "https://steamcdn-a.akamaihd.net/steam/apps/#{id}/header.jpg"] when 'gog' then [196, 392].map((x) -> "https:#{o.image}_#{x}.jpg") when 'humble' then [o.icon or o.image, o.image or o.icon] when 'ggate' then [o.icon, o.image] $append document.body, $e('link', rel: 'stylesheet', href: "https://use.fontawesome.com/releases/v5.7.0/css/all.css") GM_addStyle "#loader {position: fixed; top: 50%; left: 50%; z-index: 10000; transform: translate(-50%, -50%); font-size: 300px; text-shadow: -1px 0 grey, 0 1px grey, 1px 0 grey, 0 -1px grey}" GM_addStyle "@-webkit-keyframes rotation {from {-webkit-transform:rotate(0deg)} to {-webkit-transform:rotate(360deg)}} @keyframes rotation {from {transform:rotate(0deg) translate(-50%, -50%); -webkit-transform:rotate(0deg)} to {transform:rotate(360deg) translate(-50%, -50%); -webkit-transform:rotate(360deg)}}" GM_addStyle ".rotating {animation: rotation 2s linear infinite}" LOGO = ".logo {height: 0; width: 0; display: flex; flex-direction: row-reverse} .logo img {border: 1px solid darkorchid; background: #1b222f}" if PAGE.match RE.backloggeryUpdate SETS = objmap DATA, (o) -> objmap(o, (x) -> words x.name) legend = $get '//*[@id="content-wide"]/section/form/fieldset[1]/legend' systemDropdown = $get '//*[@id="content-wide"]/section/form/fieldset[1]/div[2]' delBtns = $get '//*[@id="content-wide"]/section/form/div[2]/div' status = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]' _system = when_ systemDropdown, (e) -> $find('select', e) swap = -> do ([a, b] = [_system, $find '#detail2 select']) -> [a.value, b.value] = [b.value, a.value] unless legend and systemDropdown and delBtns # deleted/doesn't exist backlog = GM_getValue('backlog', {}) if PARAMS.gameid in backlog delete backlog[PARAMS.gameid] GM_setValue('backlog', backlog) else for es in $find("[name=#{s}console]").children for s in ['', 'orig_'] e.innerText = "Windows" for e in es when e.value is "PC" $append systemDropdown, $e('tt', innerText: "⇄", onclick: swap, style: "cursor: pointer; padding-left: 8px; font-size: large") $before delBtns, $e('input', type: 'submit', name: 'submit2', className: 'greengray', value: "Stealth Save ⇄", onclick: swap) $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex")) $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex")) GM_addStyle ".overlay {position: relative; max-height: 0; top: -40px; z-index: 2; margin-right: 10px; display: flex; flex-direction: row-reverse} .overlay input {height: 20px; background: #4b4b4b; color: white; width: 500px; border: 1px solid black; padding-left: 1ex; margin-bottom: 0} .overlay .options {display: flex; flex-direction: column; max-height: 500px; overflow-y: auto; background: grey} .overlay button {height: 28px; background: #4b4b4b; color: white; border-radius: 10px 8px 8px 10px; margin: .5px; padding: 5px; display: flex} .overlay .trash {cursor: pointer} .overlay * {flex-shrink: 0} .overlay button b {flex: 1; padding-left: 1ex; text-align: left} .os {padding-left: .75ex; color: white; font-size: 20px} .oslist {display: flex; position: absolute; width: 505px; padding-top: 7.5px; pointer-events: none} #ignore > i {background: #4b4b4b; color: white; border: 1px solid black; cursor: pointer; font-size: 20px; padding: 2px; border-radius: 5px} #ignore > i.fa-eye {margin-left: 1.25px} #{LOGO} .logo img {height: 100px}" gameName = $find '[name=name]' _bl = GM_getValue('backlog')?[PARAMS.gameid] or {} excluded = GM_getValue('exclude', {}) data = (k=state.list) -> DATA[k] id = (s=state.list) -> "#{s}Id" id$ = (s=state.list) -> _bl[ id(s) ] eId$ = (k=id$(s), s=state.list) -> "#{s}##{k}" data$ = (s=state.list) -> data(s)?[ id$(s) ] title = (k=state.list) -> data$(k)?.name or gameName.value _order = (s=state.title, k=state.list) -> order(SETS, excluded, s, k) state = do (list = (_system.value or '').toLowerCase()) -> list: list title: title list active: no order: _order(title(list), list) when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "", statStr(o, 'completed')] _setBl = (o) -> $mergeData 'backlog', [PARAMS.gameid]: Object.assign(_bl, o) _delBl = (...ks) -> delete _bl[k] for k in ks; _setBl() _setExcl = (k, x) -> excluded[ eId$(k) ] = x $mergeData('exclude', [eId$(k)]: x) state.order = _order() section2 = $find_('fieldset')[1] section2.style.position = 'relative' $append section2, $e('div', id: 'ignore', style: "position: absolute; top: -1px; left: 110px") m.mount ignore, view: -> id$() and [ do (x = !_bl.ignore) -> m('i.far', class: "fa-eye#{if x then '' else '-slash'}", title: (if x then "Watch" else "Ignore"), onclick: -> _setBl(ignore: x)) ] $before($find_('fieldset')[0], $e('div', id: 'logo', className: 'logo')) m.mount(logo, view: -> id$() and m('a', {target: '_blank', href: data$().url}, m('img', src: $logo(state.list, id$())[1]))) document.addEventListener 'keydown', (e) -> if e.key is 'Escape' _delBl id(), 'ignore' Object.assign(state, active: no, title: title(), order: _order title()) achievements.innerText = completed.innerText = "" m.redraw() $reset = -> do (list = (_system.value or '').toLowerCase()) -> if list isnt state.list Object.assign(state, {list}, active: no, title: title(list), order: _order(title(list), list)) m.redraw() $$ = (k) -> -> Object.assign(state, active: no, title: data()[k].name) state.order = _order() _setBl([id()]: k) when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "", statStr(o, 'completed')] no gameName.onchange = _system.onchange = $reset overlay = $e('div', style: "display: flex; flex-direction: column") $after(legend, $e('div', {className: 'overlay'}, overlay)) worksOn = (o) -> (do ([s, cls] = OS[c]) -> m("i.fab.#{cls}.os", title: s)) for c in (o.worksOn or '').split '' m.mount overlay, view: -> do (o = data()) -> o and [ m('input', value: state.title, title: id$() or "", onclick: (-> state.active = yes), \ oninput: (e) -> _delBl id(), 'ignore'; state.title = e.target.value; state.order = _order()) id$() and m('.oslist', m('div', style: "flex: 1"), worksOn o[ id$() ]) state.active and m('.options', state.order.map (k) -> do (x = not excluded[ eId$(k) ]) -> [ m('button', {key: k, disabled: not x, onclick: $$(k)} m('i.trash.fas', class: "fa-trash#{if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \ onclick: $stop(-> _setExcl k, x)) m('b', o[k].name), worksOn o[k]) ]) ] else if PAGE.match RE.backloggeryCreate BACKLOG = GM_getValue 'backlog', {} MATCHED = objmap DATA, (_, s) -> setFn (x["#{s}Id"] for k, x of BACKLOG when x["#{s}Id"]) UNMATCHED = objmap DATA, (o, s) -> pick(o, (k for k of o when not MATCHED[s](k))...) SETS = objmap UNMATCHED, (o) -> objmap(o, (x) -> words x.name) excluded = GM_getValue 'exclude', {} GM_addStyle ".os {padding-left: 1ex; line-height: 0; font-size: 16px} #names {position: absolute; max-height: 500px; width: 730px; top: 75px; left: 9px; z-index: 2; display: flex; flex-direction: column; overflow-y: auto; background: grey} #names > button {flex-shrink: 0; height: 24px; border-radius: 10px; display: flex; flex-direction: row; margin-top: 1px; text-align: left; padding-left: 1ex;} #names > button > span {flex-grow: 1} #names > button > i {padding-right: .5em; color: black; cursor: pointer} #{LOGO} .logo img {height: 100px}" for es in $find("[name=#{s}console]").children for s in ['', 'orig_'] e.innerText = "Windows" for e in es when e.value is "PC" status = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]' [name, system] = ['name', 'console'].map (s) -> $find "[name=#{s}]" name.autocomplete = 'off' eId = (k, s=system.value) -> "#{s.toLowerCase()}##{k}" for e in system.children when_(UNMATCHED[ e.value.toLowerCase() ], (o) -> e.innerText += " (+#{(k for k of o when not excluded[ eId(k, e.value) ]).length})") $after($find('.info.help', detail2), $e('span', id: 'oslist', style: "padding-left: 1ex")) $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex")) $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex")) $find_('fieldset')[0].style.position = 'relative' $after($find('[name=name]'), $e('div', id: 'names')) data = (k=system.value) -> UNMATCHED[ k.toLowerCase() ] or {}; _order = (text=name.value, k=system.value) -> order(SETS, excluded, text, k.toLowerCase()) state = id: null, active: no, order: _order() _setExcl = (k, x) -> excluded[ eId(k) ] = x $mergeData('exclude', [eId(k)]: x) state.order = _order() _redraw = (id=state.id, o=data()[id]) -> $clear oslist o and $append oslist, ((do ([s, cls] = OS[c]) -> $e('i', className: "fab #{cls} os", title: s)) for c in (o.worksOn or '').split(''))... [achievements.innerText, completed.innerText] = [o?.achievements or "", statStr(o or {}, 'completed')] $before($find_('fieldset')[0], $e('div', id: 'logo', className: 'logo')) m.mount logo, view: -> do (k = system.value.toLowerCase(), o = data()[state.id]) -> o and m('a', {target: '_blank', href: o.url}, m('img', src: $logo(k, state.id)[1])) $upd = (id) -> _redraw(id); when_ data()[id], (o) -> name.value = o.name Object.assign state, {id}, active: not id, order: _order() m.redraw() name.oninput = name.onclick = -> $upd() system.onchange = -> Object.assign state, id: null, order: _order() _redraw(); m.redraw() document.addEventListener 'keydown', (e) -> if e.key is 'Escape' Object.assign state, id: null, active: no _redraw(); m.redraw() m.mount names, view: -> state.active and state.order.map (k) -> do (x = not excluded[eId(k)]) -> m 'button', {key: k, disabled: not x, onclick: -> $upd(k)}, m('span', data()[k].name), m 'i.fas', class: "fa-trash${if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \ onclick: $stop(-> $keepScroll names, -> _setExcl(k, x)) else if PAGE.match RE.backloggeryLibrary INCOMPLETE = ["(-)", "(u)", "(U)"] SLUGS = objmap DATA, slugs $assertEq(Object.keys(DATA.steam).length, Object.keys(SLUGS.steam).length, (n, m) -> "Steam names have #{n-m} collisions!") $assertEq(Object.keys(DATA.gog).length, Object.keys(SLUGS.gog).length, (n, m) -> "GOG names have ${n-m} collisions!") UPD = GM_getValue 'updated', {} $s = (e) -> e.innerText.trim() info = (e) -> $find '.gamerow', e name = (e) -> $find 'b', e id = (e) -> query($find('a', e).href).gameid $achievements = (e) -> $find '.info span', info e $completion = (e) -> $find 'img', $find_('h2 a', e)[1] $type = (s, e) -> $s(info e).match RegExp("\\b#{s}\\b", 'i') $slug = (k, e) -> SLUGS[k][ slugify($s name e) ] overlay = $e('div', style: "z-index:2; pointer-events:none; position:fixed; top:0; left:0; width:100%; height:100%; display:flex") $append document.body, overlay GM_addStyle "#{LOGO} .logo img {max-height: 62px} .logo.steam img {max-height: 67px} .logo.gog img {max-height: 64px} .os {font-weight: 100; padding-left: .75ex; line-height: 0 !important; font-size: 20px; position: relative; top: 2.5px} section.gamebox.processed .logo img {max-height: 64px} .tooltip {margin: auto; align-items: center; display: flex; flex-direction: column; background: rgba(0, 0, 0, 0.8); padding: 2em; transform: translateZ(0) translateX(-99px)}" overlayData = null $$ = (x) -> -> overlayData = x; m.redraw() m.mount overlay, view: -> overlayData and m '.tooltip', m('img', src: overlayData.image, style: "max-width: 548px") m('pre', {style: "padding-top:1em; font-weight:bold"}, overlayData.stats) $tweak = (e, k, id, [ignore, markParam, markCond], [append, appendFmt=identity], [stats, statsUpdated]) -> [[icon, image], x] = [$logo(k, id), DATA[k][id]] _data = {image, stats: (stats and "#{stats}\nUpdated: #{new Date(statsUpdated or UPD[k])}")} e.style.background = if ignore then 'darkgrey' else unless markCond(markParam e) then '' else 'lightcoral' name(e).title = "#{x.name}\nUpdated: #{new Date UPD[k]}" name(e).innerHTML += unless append then '' else " [#{appendFmt append}]" (do ([s, cls] = OS[c]) -> $append name(e), $e('i', className: "fab #{cls} os", title: s)) for c in (x.worksOn||'').split('') $before e, $e('div', {className: "logo #{k}", onmouseleave: $$(), onmouseenter: $$ _data}, $e('a', {target: '_blank', href: x.url}, $e('img', src: icon))) _renameWindows = (s) -> replace(s, /^PC( \(.*\))?$/, "Windows$1") or replace(s, /^(.*)\(PC\)$/, "$1(Windows)") or s content.addEventListener 'DOMNodeInserted', ({target}) -> if $hasClass(target, 'gamebox') and info target backlog = GM_getValue 'backlog', {} _id = id target _bl = backlog[_id] = Object.assign(backlog[_id] or {}, name: $s name target) $syncId = (k) -> _bl["#{k}Id"] = _bl["#{k}Id"] or $slug(k, target); DATA[k][ _bl["#{k}Id"] ] _type = $find 'b', info(target) _type.innerText = _renameWindows _type.innerText if $type 'steam', target data = $syncId 'steam' stats = data?.achievements _markCond = (e) -> stats is '?' or (not e and stats isnt "0 / 0") or (e and not e.innerText.startsWith "Achievements: #{stats} (") data && $tweak target, 'steam', _bl.steamId, [_bl.ignore, $achievements, _markCond], [data.hours_forever, (s) -> "#{s}h"], [statStr(data, 'achievements'), UPD['steam-stats']] else if $type 'gog', target data = $syncId 'gog' completed = data?.completed is 'yes' data and $tweak target, 'gog', _bl.gogId, [_bl.ignore, $completion, (e) -> completed is INCOMPLETE.includes e.alt], [data.rating, (n) -> "#{n/10}/5"], [statStr(data, 'completed', 'category')] else if $type 'humble', target data = $syncId 'humble' data and $tweak target, 'humble', _bl.humbleId, [_bl.ignore, (->''), (->'')], [not data.icon and "Humble Trove"], [statStr(data, 'developer', 'publisher')] else if $type 'ggate', target data = $syncId 'ggate' data and $tweak target, 'ggate', _bl.ggateId, [_bl.ignore, (->''), (->'')], [], [statStr(data, 'developer', 'publisher')] GM_setValue 'backlog', backlog else if PAGE.match RE.steamLibrary $update 'steam', fromPairs ([o.appid, pick(o, 'link', 'logo', 'name', 'hours_forever')] for o in rgGames) else if PAGE.match RE.steamRecent stats = ([x.appid, "#{x.ach_completed} / #{x.ach_total}"] for x in rgGames when x.ach_completion) $markUpdate 'steam-stats' $mergeData 'steam-stats', fromPairs stats alert "Game library interop: updated #{stats.length} games" else if PAGE.match RE.steamAchievements when_ $find('#topSummaryAchievements'), (e) -> do (id = $find('.gameLogo a').href.match(/\d+$/)[0]) -> $mergeData('steam-stats', [id]: e.innerText.match(/(\d+) of (\d+)/)[1..].join(" / ")) else if PAGE.match RE.steamDetails ID = PAGE.match(RE.steamDetails)[1] if $find '.game_area_already_owned' platforms = $find_('.platform_img', $find '.game_area_purchase_game') worksOn = (s[0] for s in ['win', 'linux', 'mac'] when platforms.some (e) -> $hasClass(e, s)).join('') worksOn && $mergeData('steam-platforms', [ID]: worksOn) else if PAGE.match RE.steamDbDetails unless $find('.panel-ownership').hidden info = $find('.span8') id = $find_('td', info)[1].innerText worksOn = (s[0] for s in ['windows', 'linux', 'macos'] when $find(".icon-#{s}", info)).join('') worksOn and $mergeData('steam-platforms', [id]: worksOn) else if PAGE.match RE.steamDbLibrary document.addEventListener 'DOMNodeInserted', ({target}) -> when_ target.id?.match?(/^js-hover-app-([0-9]+)$/), ([_, id]) -> worksOn = (s[0] for s in ['windows', 'linux', 'macos'] when $find(".icon-#{s}", target)).join(''); worksOn && $mergeData('steam-platforms', [id]: worksOn) else if PAGE.match RE.steamStats _achievements = (ss) -> (s.match(/\d+/)?[0] or s for s in ss).join(" / ") stats = if PARAMS.DisplayType isnt '2' then do (_table = $find '.tablesorter') -> # list _header = $find_ 'th', $find('thead tr', _table) _body = ($find_('td', e) for e in $find_ 'tr', $find('tbody', _table)) [_name$, _total$, _my$] = (_header.findIndex((e) -> e.innerText is s) for s in ["Name", "Total\nAch.", "Gained\nAch."]) _body.map (l) -> [$find('.content', l[_name$]).href.match(/^steam:\/\/run\/([0-9]+)$/)[1], _achievements(e.innerText for e in [l[_my$], l[_total$]])] else do (_body = $get '/html/body/center/center/center/center') -> # table _table = Array.from(_body.children).find((x) -> x.tagName is 'TABLE' and not x.classList.contains 'Pager') _ids = (query(e.href).AppID for e in $find_('a', _table)) [_ids[i], _achievements( last($find_ 'p', e).innerText.match(/Achievements: (.*) of (.*)/)[1..] )] for e, i in $find_('table', _table) $markUpdate 'steam-stats' $mergeData 'steam-stats', fromPairs stats alert "Game library interop: updated #{stats.length} games" else if PAGE.match RE.gogLibrary queryPage = (page=0) -> $query "/account/getFilteredProducts?mediaType=1&page=#{page+1}" worksOn = (o) -> o and worksOn: (k[0].toLowerCase() for k, v of o when v).join('') scrape = -> queryPage().then (o) -> do (completed = o.tags.find((x) -> x.name.toLowerCase() is 'completed').id) -> Promise.all([Promise.resolve(o), [1...o.totalPages].map(queryPage)...]).then (data) -> games = [].concat(data.map((x) => x.products)...) .map (o) => [o.id, merge(pick(o, 'image', 'rating', 'url'), worksOn(o.worksOn), name: o.title, category: o.category or undefined, completed: o.tags.includes completed)] $update 'gog', fromPairs games $append $find('.collection-header'), $e('i', className: "fas fa-sync-alt _clickable account__filters-option", title: "Sync Backloggery", onclick: scrape) else if PAGE.match RE.humbleLibrary PLATFORMS = windows: 'w', linux: 'l', osx: 'm', android: 'a' url = -> ($find('.details-heading a') or {}).href platformSelector = -> $find '.js-platform-select-holder' worksOn = -> do (e = platformSelector()) -> (PLATFORMS[k] for k of PLATFORMS when e.querySelector '.hb-'+k).join('') scrape = -> for e in $find_ '.subproduct-selector' e.click() name: $find('h2', e).innerText publisher: $find('p', e).innerText icon: $find('.icon', e).style.backgroundImage.match(/^url\("(.*)"\)$/)?[1] url: url() worksOn: worksOn() GM_addStyle "#syncBackloggery {position: absolute; top: 28px; left: 400px; cursor: pointer}" $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating")) $visibility loader, off main = $find '.base-main-wrapper' main.style.position = 'relative' $append main, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: -> $visibility loader, on setTimeout -> $update 'humble', fromPairs scrape().map (x) -> x.worksOn and [slugify(x.name), x] $visibility loader, off) $visibility syncBackloggery, off setInterval (-> when_ $find('#switch-platform'), (e) -> $visibility(syncBackloggery, (e.value is 'all') and not search.value)), 100 else if PAGE.match RE.humbleTrove name = -> $find('.product-human-name').innerText credits = (t) -> $find(".#{t}")?.innerText.trim() # t in {'dev', 'pub'} worksOn = -> (e.getAttribute('data-platform')[0] for e in $find('.platforms').children).join('') scrape = -> $find_('#trove-main .trove-grid-item').reduce ((p, e) -> p.then (l) -> e.click() l.push(name: name(), developer: credits('dev'), publisher: credits('pub'), image: $find('img', e).src, worksOn: worksOn()) $find('.dismiss-action').click() return new Promise (resolve) -> setTimeout (-> resolve l), 200 ), Promise.resolve [] $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating", style: "color: white")) $visibility loader, off $before $find('.trove-sorter').firstElementChild, $e('i', className: "fas fa-sync-alt", style: "cursor: pointer", title: "Sync Backloggery", onclick: -> $visibility loader, on scrape().then (xs) -> $update 'humble-trove', fromPairs([slugify(x.name), x] for x in xs) $visibility loader, off) else if PAGE.match RE.ggateLibrary PLATFORMS = pc: 'w', linux: 'l', mac: 'm', android: 'a' worksOn = (e) -> x.src.match(/inline_(pc|linux|mac|android)\.png$/)?[1] for x in $find_ 'img', e loadImage = (id) -> new Promise (resolve) -> wait = ({target}) -> when_ target.firstChild and $find('.boximg', target), (img) -> [dev, pub] = ["Developer", "Publisher"].map (s) -> $get("""//li[span = "#{s}: "]//a""", target)?.innerText lib_rightcol_info.removeEventListener 'DOMNodeInserted', wait setTimeout -> resolve [img.src, dev, pub] lib_rightcol_info.addEventListener 'DOMNodeInserted', wait Library.loadinfo 'game', "sku=#{id}&tab=details" scrape = -> $find_('.mygame_item').map((e) -> $find_('a.ttl', e)) .reduce ((p, [icon, name]) -> p.then (o) -> do (id = query(icon.href).sku) => loadImage(id).then ([image, developer, publisher]) -> Object.assign(o, [id]: { image, developer, publisher, name: name.title, icon: $find('img', icon).src, worksOn: worksOn(name).map((s) -> PLATFORMS[s]).join('') }) ), Promise.resolve {} GM_addStyle "#syncBackloggery {cursor: pointer; padding: 1ex}" $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating")) $visibility loader, off when_ $find('h1.icon'), (e) -> $append e, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: -> $visibility loader, on scrape().then (o) -> $update 'ggate', o $visibility loader, off) setInterval (-> $visibility syncBackloggery, window.location.pathname is '/account/games' and $find('[name=platform][value=""]')?.checked), 100 ]]>).toString(); eval( CoffeeScript.compile(inline_src) );