Download steam images

Little Python program to download your images from your Steam gallery.

For each file it just downloads the images by its id and attempts to include some meta data about the image in a separate json file. For example:

{"Id": 1474149818, "Game": "Crypt of the NecroDancer", "Desc": "Bat out of hell", "Url": "https://steamuserimages-a.akamaihd.net/ugc/974353539926239721/3591AA940A42F9217105C02EEA90190796893CED/"}

Code uses requests and has been tested with Python 3.7 on Windows.

import requests
import re
from pathlib import Path
import json
from typing import List


# Because I'm lazy and probably the only person to ever run this
STEAM_ID = 'Ardren'
DOWNLOAD_DIRECTORY = r'c:\Users\Matthewd\Dropbox\Screenshots-Steam'


def scan_gallery(username):
    file_ids = []
    page = 1

    while True:
        url = f"https://steamcommunity.com/id/{username}/screenshots/?p={page}&sort=newestfirst&browsefilter=myfiles&view=grid&privacy=14"
        r = requests.get(url)

        matches = list(re.finditer(r'<a href="https://steamcommunity\.com/sharedfiles/filedetails/\?id=(\d+)"', r.text))

        if len(matches) == 0:
            print(f"No more files found on page {page}")
            break

        print(f"Found {len(matches)} images on page {page}")

        for match in matches:
            file_ids.append(int(match.group(1)))

        page += 1

    return file_ids


def download_file_ids(base_directory: Path, file_ids: List[int]):
    for file_id in file_ids:

        download_filename = base_directory / f"{file_id}.jpeg"
        metadata_filename = base_directory / f"{file_id}.json"

        if download_filename.exists():
            print(f"Already downloaded {download_filename}")
            continue

        url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={file_id}"
        r = requests.get(url)

        match = re.search('<a href="(https://steamuserimages-a.akamaihd.net/ugc/[^/]+/[^/]+/)" target=', r.text)
        if match is None:
            print(f"Unable to find image_src for file_id {file_id}")
            continue

        create_metadata(metadata_filename, file_id, r.text)

        with download_filename.open('wb+') as f:
            r = requests.get(match.group(1))
            bytes_written = f.write(r.content)
            print(f"Downloaded {bytes_written} bytes to {download_filename}")


def create_metadata(metadata_filename: Path, file_id: int, html_detail_contents: str):
    game_name_match = re.search('<h3 class="ellipsis apphub_responsive_menu_title">([^<]+)</h3>', html_detail_contents)
    game_desc_match = re.search('<textarea class="descField" name="description" id="description" class="dynInput" maxlength="140">([^<]*)</textarea>', html_detail_contents)
    image_url_match = re.search('<a href="(https://steamuserimages-a.akamaihd.net/ugc/[^/]+/[^/]+/)" target="_blank">', html_detail_contents)

    data = {
        'Id': file_id,
        'Game': game_name_match.group(1),
        'Desc': game_desc_match.group(1),
        'Url': image_url_match.group(1)
    }

    with metadata_filename.open('w+') as f:
        json.dump(data, f)


if __name__ == '__main__':
    download_file_ids(Path(DOWNLOAD_DIRECTORY), scan_gallery(STEAM_ID))

Posted on

Website update

Hey look, HTTPS and colors

Edit: and block-quotes, and tables, and responsive images, and page titles

Posted on

Website back up

Website back up today after long period of down time.

I think I would like to go back to recording things I do.

Even if they are small and useless, I think it would be heathy for me.

Posted on

TP-Link TD-W8961N

  • Product Link
  • Don’t buy one
  • If you’re too lazy to replace it, and you want to monitor your ADSL stats when it rains…

tp-w8961n-status.py

import telnetlib
import re
import time


class TPW8961N(object):

    def __init__(self, host):
        self.host = host

    def __enter__(self):
        self.con = telnetlib.Telnet(self.host, 23, 5)
        self.con.read_until('Password: ')
        self.con.write('admin\n')
        self.con.read_until('TP-LINK> ')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.con.write('exit\n')
        self.con.read_all()

    def send_cmd(self, cmd):
        self.con.write(cmd.get_cmd() + '\n')
        response = self.con.read_until('TP-LINK> ')
        return cmd.parse(response)


class Command(object):
    def __init__(self):
        pass

    def get_cmd(self):
        pass

    def parse(self, string):
        pass


class StatusCommand(Command):
    def get_cmd(self):
        return 'wan adsl status'

    def parse(self, response):
        match = re.search('current modem status: (.*?)\r\n', response)
        if match is not None and match.group(1) == 'up':
            return 'up'
        else:
            return 'down'


class RateCommand(Command):
    def get_cmd(self):
        return 'wan adsl c'

    def parse(self, response):
        dl_match = re.search('near-end interleaved channel bit rate: (.*?) kbps\r\n', response)
        ul_match = re.search('far-end interleaved channel bit rate: (.*?) kbps\r\n', response)

        if dl_match is None or ul_match is None:
            return {'dl_rate': 0, 'ul_rate': 0}

        return {'dl_rate': int(dl_match.group(1)), 'ul_rate': int(ul_match.group(1))}


class QualityCommand(Command):
    def __init__(self, direction):
        if direction not in ['downstream', 'upstream']:
            raise Exception('Unknown direction, must be "downstream" or "upstream"')
        self.direction = direction
        super(Command, self).__init__()

    def get_cmd(self):
        if self.direction == 'downstream':
            return 'wan adsl l n'
        return 'wan adsl l f'

    def parse(self, response):
        noise_match = re.search('noise margin ' + self.direction + ': (.*?) db\r\n', response)
        attenuation_match = re.search('attenuation ' + self.direction + ': (.*?) db\r\n', response)

        if noise_match is None or attenuation_match is None:
            return {'noise': 0.0, 'attenuation': 0.0}

        return {'noise': float(noise_match.group(1)), 'attenuation': float(attenuation_match.group(1))}


if __name__ == '__main__':

    while True:
        try:
            print time.strftime('%Y-%m-%d %H:%M-%S')
            with TPW8961N('192.168.1.1') as modem:
                print modem.send_cmd(StatusCommand())
                print modem.send_cmd(RateCommand())
                print modem.send_cmd(QualityCommand('downstream'))
                print modem.send_cmd(QualityCommand('upstream'))
            time.sleep(60)
        except KeyboardInterrupt:
            raise
        except e:
            print e

Posted on

Picking a Practice Lock

I bought a transparent practice lock (5 pin with spools) from Massdrop a while ago, and thanks to some procrastinating had a proper go at it today. While it is just a practice lock, I struggled judging the correct tension when dealing with false sets, and the whole process felt more delicate than a basic lock. I feel like I might have given up on a standard lock before understanding how the lock behaved and why. Still, I need much, much more practice.

(Massdrop is running another drop for it now)

Posted on

KBT Pure Pro 60% Keyboard Layout

There’s not that much information about the (Vortex) KBT Pure Pro keyboard on the internet so I thought I’d add my notes here.

Layout:
  1x15
  1.5 1x12 1.5
  1.75 1x11 2.25
  2 1x13
  1.25 1x2 1.25 4.5 1x6

(1 unit = 0.75" or about 19mm)
Notable weird key sizes:
  \          1
  Backspace  1
  Del        1.5
  L Shift    2
  R Shift    1
  R Ctrl     1
  System     1
  Space      4.5
  R Alt      1

I’ve been playing with using Lua to generate parts of my wiki pages, below is the code to generate a simple SVG of the Pure Pro’s layout.

keyboard_svg_layout.lua

--noescape--

local tag = doku.xml_tag

local unit = 19*2  -- Unit size, mapped straight to pixels
local space = 4    -- Spacing between keys (as pixels again)

local halfspace = space/2
local doublespace = space*2

local fontsize = 10

local layout = {
  {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5},
  {1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25},
  {2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.25, 1, 1, 1.25, 4.5, 1, 1, 1, 1, 1, 1}
}

local keys = {
  {'esc', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\\', 'BS'},
  {'tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'del'},
  {'caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',' l', ';', '\'', 'enter'},
  {'shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'shift', 'up', 'rctrl'},
  {'lctrl', 'fn', 'sys', 'lalt', 'space', 'ralt', 'fn', 'pn', 'left', 'down', 'right'},
}

local scene = {}

local background = tag('rect', {width="100%", height="100%", fill="#ddd", stroke="black"})
table.insert(scene, background)

for row=1, #layout do
  x = unit
  for col=1, #layout[row] do

    local rect = tag('rect', {x=x+halfspace,
                             y=(row*unit)+halfspace,
                             rx=halfspace,
                             ry=halfspace,
                             width=(layout[row][col]*unit)-space,
                             height=unit-space,
                             fill="#fff",
                             stroke="black"})

    table.insert(scene, rect)


    local sizetext = tag('text', {x=x+doublespace,
                                  y=((row)*unit)+space+fontsize,
                                  fill="#d11",
                                  style='font-size: ' .. fontsize .. 'px'},
                                  tostring(layout[row][col]))

    table.insert(scene, sizetext)


    if keys[row] and keys[row][col] then
      local keytext = tag('text', {x=x+doublespace,
                                  y=((row)*unit)+(2*fontsize)+(3*space),
                                  fill="#111",
                                  style='font-size: ' .. fontsize .. 'px'},
                                  keys[row][col])

      table.insert(scene, keytext)
    end

    x = x + (layout[row][col]*unit)
  end
end

-- Keyboard is 15 units by 5 units (+1 each side for padding)
print(tag('svg', {width=(unit*17), height=(unit*7)}, scene))

SVG Output

1esc111213141516171819101-1=1\1BS1.5tab1q1w1e1r1t1y1u1i1o1p1[1]1.5del1.75caps1a1s1d1f1g1h1j1k1 l1;12.25enter2shift1z1x1c1v1b1n1m1,1.1/1shift1up1rctrl1.25lctrl1fn1sys1.25lalt4.5space1ralt1fn1pn1left1down1right

Posted on

LuaJIT Benchmarks

I’ve been playing with Lua a little bit recently and was interested in getting an idea of the performance difference between Lua 5.3 and LuaJIT. (The LuaJIT website has very good page covering benchmarks. I just wanted to play around myself). Like the LuaJIT page I picked a few example programs from The Computer Language Benchmarks Game. Programs were run on a AMD-6300FX running Windows 8.

nbody.lua-4.lua 50000000

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 469.36 0.05 467.05 2,356
luajit203 26.28 0.00 26.20 2,504
luajit21 26.31 0.00 26.25 2,512

fannkuchredux.lua 12

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 1717.53 0.14 1710.78 2,284
luajit203 105.33 0.00 105.16 2,528
luajit21 106.06 0.00 105.91 2,536

spectralnorm.lua 5500

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 199.65 0.03 199.38 2,864
luajit203 4.89 0.00 4.88 2,724
luajit21 4.89 0.00 4.88 2,728

binarytrees.lua-2.lua 2

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 439.98 13.95 423.66 1,072,400
luajit203 78.34 2.22 75.86 928,076
luajit21 77.92 1.75 75.91 811,984

Summary Relative increase in speed over Lua 5.3.

Benchmark lua53 luajit203 luajit2
nbody.lua-4.lua 50000000 1.00 17.83 17.79
fannkuchredux.lua 12 1.00 16.27 16.15
spectralnorm.lua 5500 1.00 40.86 40.86
binarytrees.lua-2.lua 20 1.00 5.58 5.58

Posted on

Needless Abuse of Lua

So, this is pretty awful. Cool too. Bust mostly awful.

bestoptionsna.lua

local function setoptions(f)
    local options = {}
    local currentname = nil
    local env = setmetatable({},
    {
        __index = function(self, method)
            if method == 'set' then
                return function(name)
                    if currentname ~= nil then
                        error('need to call "to" first')
                    end
                    currentname = name
                end
            elseif method == 'to' then
                return function(value)
                    if currentname == nil then
                        error('need to call "set" before "to"')
                    end
                    options[currentname] = value
                    currentname = nil
                end
            elseif method == 'enable' then
                return function(name)
                    options[name] = true
                end
            end
        end
    })

    -- close enough to fsetenv
    load(string.dump(f), nil, nil, env)()

    return options
end


local function pptable(t)
    for k,v in pairs(t) do
        print(k, '=', v)
    end
end

-- Cool way
local options = setoptions(function()
    set "port" to "8080"
    set "listen" to "127.0.0.1"
    enable "debug"
end)

pptable(options)


-- Boring way
options = {port='8080', listen='127.0.0.1', debug=true}

pptable(options)

Output

debug	=	true
port	=	8080
listen	=	127.0.0.1

debug	=	true
port	=	8080
listen	=	127.0.0.1

Posted on

Useless Lua Code

Got a hex dump of some firmware which was in kinda an odd format (offsets, data, checksum(maybe?) in hexadecimal ascii text). Wrote a small script to dump the printable characters. Not really interesting.

dump-text.lua

local function readsection(f)
    local colon = f:read(1)
    if colon == nil then return nil end
    if colon ~= ':' then error('section did not start with colon') end
    local data = ''
    while true do
        local char = f:read(1)
        if(char:byte() == 13) then
            f:read(1) -- eat cr
            return data
        end
        data = data .. char
    end
end


local function decodecharacter(c)
    if c:len() ~= 1 then error('must be single character') end
    local map = {['0']=0, ['1']=1, ['2']=2, ['3']=3, ['4']=4, ['5']=5, ['6']=6,
                 ['7']=7, ['8']=8, ['9']=9, A=10, B=11, C=12, D=13, E=14, F=15}
    return map[c]
end


local function decodepair(s)
    if s:len() ~= 2 then error('must be two characters in length') end
    return (decodecharacter(s:sub(1,1)) * 16) + decodecharacter(s:sub(2,2))
end


local function decodesection(s)
    local offset = {}
    local data = {}

    -- todo: last two chars = checksum(offset+data)?
    for i=1, s:len(), 2 do
        local pair = s:sub(i, i+1)
        local value = decodepair(pair)

        if i <= 8 then
            table.insert(offset, value)
        else
            table.insert(data, value)
        end
    end
    return offset, data
end


local function safeprintdata(data)
    local chars = {}
    for index, byte in ipairs(data) do
        if byte >= 32 and byte <= 126 then
            table.insert(chars, string.char(byte))
        else
            table.insert(chars, '.')
        end
    end
    return table.concat(chars)
end


local f = io.open('P2008-V-02.08.02.hex', 'rb')

while true do
    line = readsection(f)
    if line == nil then break end
    local offset, data = decodesection(line)
    print(string.format('%-42s  %s', line, safeprintdata(data)))
end

Output

102170005B204D317878205D2020000000000000B9  [ M1xx ]  .......
102180005B204D313133205D202000000000000035  [ M113 ]  ......5
10219000562D30322E30382E3032202000000000F4  V-02.08.02  .....
1021A00028632920616C6973746172207379737478  (c) alistar systx
1021B000656D732C20323030312D32303132202099  ems, 2001-2012  .

Posted on

Crimsonland Pak Format Again

Have been spending a little bit of time playing with Lua and an early project was to rewrite the Python script for reading a Crimsonland Pak file. So far I’m enjoying Lua and its language design, even with the very limited standard libraries.

clpak.lua

BinaryFile = {}

BinaryFile.new = function(file)
    return setmetatable({file=file}, {__index=BinaryFile})
end

BinaryFile.readint32 = function(self)
    local data = self.file:read(4)
    return (data:byte(4) * (256^3)) +
           (data:byte(3) * (256^2)) +
           (data:byte(2) * (256^1)) +
           (data:byte(1) * (256^0))
end

BinaryFile.readnulstring = function(self)
    local buf = ''
    while true do
        local ch = self.file:read(1)
        if ch:byte() == 0 then break end
        buf = buf .. ch
    end
    return buf
end

BinaryFile.read = function(self, ...)
    return self.file:read(...)
end

BinaryFile.skip = BinaryFile.read

BinaryFile.seek = function(self, ...)
    return self.file:seek(...)
end


local readindex = function(filename)
    local f  = io.open(filename, 'rb')
    local bf = BinaryFile.new(f)

    -- Header
    assert(bf:readnulstring() == 'PAK')
    assert(bf:readnulstring() == 'V11')

    -- index offsets
    local indexstartoffset = bf:readint32()
    local indexendoffset   = bf:readint32()

    -- just to index
    bf:seek('set', indexstartoffset)

    -- number of entries
    local indexsize = bf:readint32()

    local index = {}

    for i=1, indexsize do
        table.insert(index, {
            name   = bf:readnulstring(),
            offset = bf:readint32(),
            length = bf:readint32()
        })

        -- Unknown (junk?)
        bf:skip(8)
    end

    f:close()

    return index
end


local index = readindex('D:\\Steam\\steamapps\\common\\Crimsonland\\data.pak')

for _,v in ipairs(index) do
    print(string.format('name:%s offset:%d length:%d', v.name, v.offset, v.length))
end

Posted on