S Smith revised this gist . 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> >{% 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