tf_item_list.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/python3
  2. import base64
  3. import chevron
  4. import configparser
  5. import datetime
  6. import hashlib
  7. import html.parser
  8. import io
  9. import json
  10. import urllib
  11. import urllib.request
  12. import urllib.parse
  13. template_string = r'''
  14. <!DOCTYPE html>
  15. <html>
  16. <head>
  17. <meta charset="UTF-8">
  18. <title>TF2 Item List</title>
  19. <style>
  20. html { scroll-behavior: smooth; }
  21. body {
  22. font-family: Verdana,Arial,sans-serif;
  23. font-size: 0.75em;
  24. background-color: #111;
  25. color: #FFF;
  26. }
  27. td {
  28. padding: 0.25em;
  29. }
  30. a {
  31. color: #1eb;
  32. }
  33. .item {
  34. vertical-align: top;
  35. }
  36. .header {
  37. font-weight: bold;
  38. position: sticky;
  39. top: 0;
  40. background-color: #111;
  41. }
  42. .footer {
  43. margin-top: 2.5em;
  44. text-align: center;
  45. }
  46. tr:nth-child(even) { background: #222; }
  47. tr:hover { background: #333; }
  48. .item-list { width: 100%; }
  49. tr:target {
  50. box-shadow: 0 0 1em .25em #abb;
  51. position: relative;
  52. }
  53. .id a {
  54. color: inherit;
  55. text-decoration: none;
  56. display: block;
  57. }
  58. .q_0 { color: #B2B2B2; }
  59. .q_3 { color: #476291; }
  60. .q_5 { color: #8650AC; }
  61. .q_6 { color: #FFD700; }
  62. .q_11 { color: #CF6A32; }
  63. .q_15 { color: #FAFAFA; }
  64. </style>
  65. </head>
  66. <body>
  67. <h1>List of Team Fortress 2 items:</h1>
  68. <p>Page generated for schema last modified {{update_time}}. <a href="" download>Click here to download this page for offline viewing.</a></p>
  69. <table class="item-list">
  70. <thead>
  71. <tr class="item header">
  72. <th class="id">ID</th>
  73. <th class="name">Internal Name</th>
  74. <th class="en_name">Localized Name</th>
  75. <th class="slot">Item Slot</th>
  76. <th class="cname">Entity</th>
  77. </tr>
  78. </thead>
  79. <tbody>
  80. {{#result.items}}
  81. <tr class="item entry" id="{{defindex}}">
  82. <td class="id"><a href="#{{defindex}}">{{defindex}}</a></td>
  83. <td class="name q_{{item_quality}}">{{name}}</td>
  84. <td>{{item_name}}</td>
  85. <td>{{item_slot}}</td>
  86. <td>{{item_class}}</td>
  87. </tr>
  88. {{/result.items}}
  89. </tbody>
  90. </table>
  91. <div class="footer">
  92. <div>
  93. Generated with <a href="https://git.csrd.science/nosoop/py-tf2-schema-renderer">this script</a>.
  94. </div>
  95. <div>
  96. Table sorting provided by <a href="https://github.com/tristen/tablesort">tablesort</a>.
  97. </div>
  98. </div>
  99. <!--
  100. - table sorting functionality
  101. - the page degrades gracefully without JS, but sorting is pretty nice to have
  102. -->
  103. {{#inline_scripts}}
  104. <script src="https://unpkg.com/tablesort@5.0.2/dist/tablesort.min.js"
  105. integrity="sha384-MAoWp5qWRu0089eW0evm9gRng72sxH0daE2mkSXN8RoQ99hG/x+Wi8GxUbrgbYLp"
  106. crossorigin="anonymous"></script>
  107. <script src="https://unpkg.com/tablesort@5.0.2/dist/sorts/tablesort.number.min.js"
  108. integrity="sha384-YPqBWGhq2JCwnOo6VsMYgE5XvUqQrzskaIGxc7CByfV6o9viRVq8uAhCs/0iPQBw"
  109. crossorigin="anonymous"></script>
  110. {{/inline_scripts}}
  111. <script>
  112. new Tablesort(document.querySelector('.item-list'));
  113. window.onload = window.onhashchange = () => {
  114. if (location.hash) {
  115. document.getElementById(location.hash.slice(1)).scrollIntoView({
  116. behavior: "auto", block: "center", inline: "center"
  117. });
  118. }
  119. };
  120. </script>
  121. </body>
  122. </html>
  123. '''
  124. def process_item_class(item):
  125. used_by = item.get("used_by_classes", [])
  126. if used_by:
  127. item['used_by_classes'] = [ { "class": c.lower() } for c in used_by ]
  128. return item
  129. def get_schema_items(params):
  130. opts = dict(params.items())
  131. opts['start'] = 0
  132. url = "https://api.steampowered.com/IEconItems_440/GetSchemaItems/v0001/"
  133. while True:
  134. with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(opts)}") as r:
  135. data = json.load(r)['result']
  136. yield from data.get('items', [])
  137. if 'next' not in data:
  138. break
  139. opts['start'] = data['next']
  140. def inline_scripts(text, render):
  141. """
  142. Helper function to parse script elements, download the scripts and inline them.
  143. """
  144. class ScriptElementParser(html.parser.HTMLParser):
  145. def __init__(self):
  146. super(ScriptElementParser, self).__init__()
  147. self.scripts = []
  148. self.current = []
  149. def handle_starttag(self, tag, attrs):
  150. if tag == 'script':
  151. self.current.append(dict(attrs))
  152. def handle_endtag(self, tag):
  153. self.scripts.append(self.current.pop())
  154. parser = ScriptElementParser()
  155. parser.feed(text)
  156. f = io.StringIO()
  157. for script in parser.scripts:
  158. with urllib.request.urlopen(script['src']) as r:
  159. data = r.read()
  160. if 'integrity' in script:
  161. ct, hash = script['integrity'].split('-', 1)
  162. h = hashlib.new(ct)
  163. h.update(data)
  164. if base64.b64encode(h.digest()).decode('ascii') != hash:
  165. raise ValueError(f"{script['src']} failed subresource integrity check {script['integrity']} (got {base64.b64encode(h.digest())})")
  166. f.write(f"<script>\n/* {script['src']} */\n{data.decode('utf8')}\n</script>\n")
  167. return f.getvalue()
  168. def schema_mtime(params):
  169. url = "https://api.steampowered.com/IEconItems_440/GetSchemaOverview/v0001/"
  170. with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(dict(params.items()))}") as r:
  171. return r.info().get('last-modified')
  172. config = configparser.ConfigParser()
  173. config.read('config.ini', encoding = "utf8")
  174. with open('items.html', 'wt', encoding = 'utf8') as f:
  175. f.write(chevron.render(template_string, {
  176. 'result': {
  177. 'items': list(map(process_item_class, get_schema_items(config['DEFAULT'])))
  178. },
  179. 'update_time': schema_mtime(config['DEFAULT']),
  180. 'inline_scripts': inline_scripts
  181. }))