123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- #!/usr/bin/python3
- import base64
- import chevron
- import configparser
- import datetime
- import hashlib
- import html.parser
- import io
- import json
- import urllib
- import urllib.request
- import urllib.parse
- template_string = r'''
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>TF2 Item List</title>
-
- <style>
- html { scroll-behavior: smooth; }
- body {
- font-family: Verdana,Arial,sans-serif;
- font-size: 0.75em;
-
- background-color: #111;
- color: #FFF;
- }
- td {
- padding: 0.25em;
- }
- a {
- color: #1eb;
- }
- .item {
- vertical-align: top;
- }
- .header {
- font-weight: bold;
- position: sticky;
- top: 0;
- background-color: #111;
- }
- .footer {
- margin-top: 2.5em;
- text-align: center;
- }
- tr:nth-child(even) { background: #222; }
- tr:hover { background: #333; }
-
- .item-list { width: 100%; }
-
- tr:target {
- box-shadow: 0 0 1em .25em #abb;
- position: relative;
- }
- .id a {
- color: inherit;
- text-decoration: none;
- display: block;
- }
-
- .q_0 { color: #B2B2B2; }
- .q_3 { color: #476291; }
- .q_5 { color: #8650AC; }
- .q_6 { color: #FFD700; }
- .q_11 { color: #CF6A32; }
- .q_15 { color: #FAFAFA; }
- </style>
- </head>
- <body>
- <h1>List of Team Fortress 2 items:</h1>
-
- <p>Page generated for schema last modified {{update_time}}. <a href="" download>Click here to download this page for offline viewing.</a></p>
-
- <table class="item-list">
- <thead>
- <tr class="item header">
- <th class="id">ID</th>
- <th class="name">Internal Name</th>
- <th class="en_name">Localized Name</th>
- <th class="slot">Item Slot</th>
- <th class="cname">Entity</th>
- </tr>
- </thead>
- <tbody>
- {{#result.items}}
- <tr class="item entry" id="{{defindex}}">
- <td class="id"><a href="#{{defindex}}">{{defindex}}</a></td>
- <td class="name q_{{item_quality}}">{{name}}</td>
- <td>{{item_name}}</td>
- <td>{{item_slot}}</td>
- <td>{{item_class}}</td>
- </tr>
- {{/result.items}}
- </tbody>
- </table>
- <div class="footer">
- <div>
- Generated with <a href="https://git.csrd.science/nosoop/py-tf2-schema-renderer">this script</a>.
- </div>
- <div>
- Table sorting provided by <a href="https://github.com/tristen/tablesort">tablesort</a>.
- </div>
- </div>
-
- <!--
- - table sorting functionality
- - the page degrades gracefully without JS, but sorting is pretty nice to have
- -->
- {{#inline_scripts}}
- <script src="https://unpkg.com/tablesort@5.0.2/dist/tablesort.min.js"
- integrity="sha384-MAoWp5qWRu0089eW0evm9gRng72sxH0daE2mkSXN8RoQ99hG/x+Wi8GxUbrgbYLp"
- crossorigin="anonymous"></script>
- <script src="https://unpkg.com/tablesort@5.0.2/dist/sorts/tablesort.number.min.js"
- integrity="sha384-YPqBWGhq2JCwnOo6VsMYgE5XvUqQrzskaIGxc7CByfV6o9viRVq8uAhCs/0iPQBw"
- crossorigin="anonymous"></script>
- {{/inline_scripts}}
- <script>
- new Tablesort(document.querySelector('.item-list'));
- window.onload = window.onhashchange = () => {
- if (location.hash) {
- document.getElementById(location.hash.slice(1)).scrollIntoView({
- behavior: "auto", block: "center", inline: "center"
- });
- }
- };
- </script>
- </body>
- </html>
- '''
- def process_item_class(item):
- used_by = item.get("used_by_classes", [])
- if used_by:
- item['used_by_classes'] = [ { "class": c.lower() } for c in used_by ]
- return item
- def get_schema_items(params):
- opts = dict(params.items())
- opts['start'] = 0
-
- url = "https://api.steampowered.com/IEconItems_440/GetSchemaItems/v0001/"
- while True:
- with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(opts)}") as r:
- data = json.load(r)['result']
-
- yield from data.get('items', [])
-
- if 'next' not in data:
- break
- opts['start'] = data['next']
- def inline_scripts(text, render):
- """
- Helper function to parse script elements, download the scripts and inline them.
- """
- class ScriptElementParser(html.parser.HTMLParser):
- def __init__(self):
- super(ScriptElementParser, self).__init__()
- self.scripts = []
- self.current = []
-
- def handle_starttag(self, tag, attrs):
- if tag == 'script':
- self.current.append(dict(attrs))
-
- def handle_endtag(self, tag):
- self.scripts.append(self.current.pop())
-
- parser = ScriptElementParser()
- parser.feed(text)
-
- f = io.StringIO()
- for script in parser.scripts:
- with urllib.request.urlopen(script['src']) as r:
- data = r.read()
- if 'integrity' in script:
- ct, hash = script['integrity'].split('-', 1)
- h = hashlib.new(ct)
- h.update(data)
- if base64.b64encode(h.digest()).decode('ascii') != hash:
- raise ValueError(f"{script['src']} failed subresource integrity check {script['integrity']} (got {base64.b64encode(h.digest())})")
- f.write(f"<script>\n/* {script['src']} */\n{data.decode('utf8')}\n</script>\n")
-
- return f.getvalue()
- def schema_mtime(params):
- url = "https://api.steampowered.com/IEconItems_440/GetSchemaOverview/v0001/"
- with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(dict(params.items()))}") as r:
- return r.info().get('last-modified')
- config = configparser.ConfigParser()
- config.read('config.ini', encoding = "utf8")
- with open('items.html', 'wt', encoding = 'utf8') as f:
- f.write(chevron.render(template_string, {
- 'result': {
- 'items': list(map(process_item_class, get_schema_items(config['DEFAULT'])))
- },
- 'update_time': schema_mtime(config['DEFAULT']),
- 'inline_scripts': inline_scripts
- }))
|