Last active 1568011293

Bandersnatch v2

S Smith revised this gist 1568047293. Go to revision

2 files changed, 186 insertions

bandersnatch.py(file created)

@@ -0,0 +1,126 @@
1 + #!/usr/bin/env python3
2 + from random import choice
3 + import json
4 + from sys import stderr
5 +
6 + with open("bandersnatch.json") as f:
7 + bandersnatch = json.load(f)
8 +
9 + with open("segmentmap.json") as f:
10 + smap = json.load(f)
11 +
12 + initial_state = { "p_sp": True, "p_tt": True, "p_8a": False, "p_td": True, "p_cs": False, "p_w1": False, "p_2b": False, "p_3j": False, "p_pt": False, "p_cd": False, "p_cj": False, "p_sj": False, "p_sj2": False, "p_tud": False, "p_lsd": False, "p_vh": False, "p_3l": False, "p_3s": False, "p_3z": False, "p_ps": "n", "p_wb": False, "p_kd": False, "p_bo": False, "p_5v": False, "p_pc": "n", "p_sc": False, "p_ty": False, "p_cm": False, "p_pr": False, "p_3ad": False, "p_s3af": False, "p_nf": False, "p_np": False, "p_ne": False, "p_pp": False, "p_tp": False, "p_bup": False, "p_be": False, "p_pe": False, "p_pae": False, "p_te": False, "p_snt": False, "p_8j": False, "p_8d": False, "p_8m": False, "p_8q": False, "p_8s": False, "p_8v": False, "p_vs": "n", "p_scs": False, "p_3ab": False, "p_3ac": False, "p_3aj": False, "p_3ah": False, "p_3ak": False, "p_3al": False, "p_3af": False, "p_5h": False, "p_5ac": False, "p_5ag": False, "p_5ad": False, "p_6c": False, "length": 0 }
13 + state = dict(initial_state)
14 +
15 + info = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]
16 + moments = info["momentsBySegment"]
17 + preconditions = info["preconditions"]
18 + segmentGroups = info["segmentGroups"]
19 + thumbnails = info["choicePointNavigatorMetadata"]["choicePointsMetadata"]["choicePoints"]
20 +
21 + segmentMap = {}
22 + for segmentList in moments.values():
23 + for _segment in segmentList:
24 + for _choice in _segment.get("choices", []):
25 + if "segmentId" in _choice and "text" in _choice:
26 + segmentMap[_choice["segmentId"]] = _choice["text"].title()
27 +
28 + def msToTS(ms):
29 + s,ms = divmod(ms,1000)
30 + m,s = divmod(s,60)
31 + h,m = divmod(m,60)
32 + return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms)
33 +
34 + def conditionHandler(cond):
35 + global state
36 + if not cond:
37 + return True
38 + if cond[0] == "persistentState":
39 + return state[cond[1]]
40 + if cond[0] == "not":
41 + return not all(conditionHandler(c) for c in cond[1:])
42 + if cond[0] == "and":
43 + return all(conditionHandler(c) for c in cond[1:])
44 + if cond[0] == "eql":
45 + return conditionHandler(cond[1]) == cond[2]
46 + if cond[0] == "or":
47 + return any(conditionHandler(c) for c in cond[1:])
48 +
49 + def groupHandler(group, segment=None):
50 + out = []
51 + if segment:
52 + group.append(segment)
53 + for item in group:
54 + if type(item) is str and conditionHandler( preconditions.get(item,[]) ):
55 + out.append(item)
56 + if type(item) is dict:
57 + if "segmentGroup" in item:
58 + out += groupHandler(segmentGroups[item["segmentGroup"]])
59 + if "precondition" in item:
60 + if conditionHandler( preconditions.get(item["precondition"],[]) ):
61 + out.append(item["segment"])
62 + # print("out="+repr(out),file=stderr)
63 + return out
64 +
65 +
66 + def followTheStory(segment):
67 + global state
68 + global history
69 + possibilities = []
70 + if segment in moments:
71 + m = moments[segment]
72 + for moment in m:
73 + if moment["type"] == "notification:playbackImpression":
74 + state.update( moment.get("impressionData",{}).get("data", {}).get("persistent", {}) )
75 + if moment["type"] == "scene:cs_bs":
76 + for option in moment["choices"]:
77 + state.update( option.get("impressionData",{}).get("data", {}).get("persistent", {}) )
78 + if "segmentId" in option:
79 + p = groupHandler([option["segmentId"]])
80 + elif "sg" in option:
81 + p = groupHandler(segmentGroups[option["sg"]])
82 + elif moment["trackingInfo"]["optionType"] == "fakeOption":
83 + continue
84 + else:
85 + raise Exception(option["id"])
86 + possibilities += p
87 + if moment["type"] == "notification:action":
88 + possibilities.append(segment)
89 + if segment in segmentGroups:
90 + possibilities += groupHandler(segmentGroups[segment])
91 + # print("poss="+repr(possibilities),file=stderr)
92 + if not possibilities:
93 + # raise Exception("hoi")
94 + possibilities += groupHandler(segmentGroups["respawnOptions"])
95 + return choice(possibilities)
96 +
97 + def get_segment_info(segment):
98 + _ = thumbnails.get(segment, {})
99 + return {"id": segment, "url": _["image"]["styles"]["backgroundImage"][4:-1] if "image" in _ else "", "caption": _.get("description", "No caption"), "chose": segmentMap.get(segment, "No caption ({})".format(segment))}
100 +
101 + def bandersnatch():
102 + global state
103 + concat, options = [], []
104 + state = dict(initial_state)
105 + current_segment = "1A"
106 + while True:
107 + state["length"] += smap[current_segment]["endTimeMs"] - smap[current_segment]["startTimeMs"]
108 + if current_segment[:3].lower() == "0cr":
109 +
110 + options.append(get_segment_info(current_segment))
111 + concat.append(current_segment)
112 +
113 + options.append(get_segment_info("IDNT"))
114 + concat.append('IDNT')
115 +
116 + state["length"] += 10
117 + break
118 + options.append(get_segment_info(current_segment))
119 + concat.append(current_segment)
120 + current_segment = followTheStory(current_segment)
121 + if current_segment is None:
122 + break
123 + return concat, options, msToTS(state["length"])
124 +
125 + if __name__ == "__main__":
126 + print(bandersnatch(),file=stderr)

srv.py(file created)

@@ -0,0 +1,60 @@
1 + #!/usr/bin/env python
2 + from flask import Flask, render_template_string, send_file
3 + from bandersnatch import bandersnatch
4 +
5 + TEMPLATE = """
6 + <!doctype html>
7 + <html>
8 + <head>
9 + <style>
10 + body {
11 + background-color: black;
12 + color: white;
13 + font-family: sans-serif
14 + }
15 + label {
16 + background-color: black;
17 + }
18 + .segment {
19 + width:472px;
20 + height:266px;
21 + }
22 + div.segment {
23 + margin:30px
24 + }
25 + video.segment {
26 + }
27 + </style>
28 + <title>Bandersnatch</title>
29 + </head>
30 + <body>
31 + <h3>{{ length }}</h3>
32 + <textarea>{% for segment in concat %}
33 + file '{{ segment }}.mkv' {% endfor %}</textarea>
34 + <div style='display: flex; flex-wrap: wrap;'>
35 + {% for option in options %}
36 + <div class='segment' id='{{ option.id }}' style='background-image: url({{ option.url }})'>
37 + <video class='segment' id='{{ option.id }}-vid' controls {% if option.url %}poster='{{ option.url }}' preload='none'{% else %}preload='metadata'{% endif %}>
38 + <source src='https://s3.home.b303.me/bandersnatch/{{ option.id }}.mkv{% if not option.url %}#t=0.5{% endif %}' type='video/mp4'>
39 + </video>
40 + <label for='{{ option.id }}'>{% if option.chose != "No caption (1A)" %}<span class='chose'>Chose {{ option.chose }}</span> &gt;{% endif %} <span class='caption'>{{ option.caption }}</span></label>
41 + </div>
42 + {% endfor %}
43 + </div>
44 + </body>
45 + </html>
46 + """
47 +
48 + app = Flask(__name__)
49 +
50 + @app.route("/<segment>.mkv")
51 + def mkv(segment):
52 + return send_file("{}.mkv".format(segment))
53 +
54 + @app.route("/")
55 + def index():
56 + concat, options, length = bandersnatch()
57 + return render_template_string(TEMPLATE, concat=concat, options=options, length=length)
58 +
59 + if __name__ == "__main__":
60 + app.run(debug=True, host="0.0.0.0", port=24572)
Newer Older