webatem 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. atem_control/__init__.py +0 -0
  2. atem_control/activity.py +37 -0
  3. atem_control/apps.py +13 -0
  4. atem_control/control/__init__.py +9 -0
  5. atem_control/control/activity.py +194 -0
  6. atem_control/control/commands.py +469 -0
  7. atem_control/control/consumer.py +1245 -0
  8. atem_control/control/device_api.py +104 -0
  9. atem_control/control/logging.py +121 -0
  10. atem_control/control/views.py +222 -0
  11. atem_control/discovery.py +362 -0
  12. atem_control/hyperdeck/__init__.py +8 -0
  13. atem_control/hyperdeck/connection.py +146 -0
  14. atem_control/hyperdeck/views.py +197 -0
  15. atem_control/ip_upload_lock.py +32 -0
  16. atem_control/media_pool/__init__.py +36 -0
  17. atem_control/media_pool/broadcast.py +79 -0
  18. atem_control/media_pool/views.py +155 -0
  19. atem_control/media_pool/watcher.py +1348 -0
  20. atem_control/migrations/0001_initial.py +34 -0
  21. atem_control/migrations/0002_switcher.py +26 -0
  22. atem_control/migrations/0003_remove_atemcontrollog_atem_contro_user_id_3d6bb9_idx_and_more.py +29 -0
  23. atem_control/migrations/0004_delete_switcher.py +16 -0
  24. atem_control/migrations/__init__.py +0 -0
  25. atem_control/models.py +45 -0
  26. atem_control/netutil.py +36 -0
  27. atem_control/profile/__init__.py +5 -0
  28. atem_control/profile/dialog.py +521 -0
  29. atem_control/profile/export.py +406 -0
  30. atem_control/profile/views.py +444 -0
  31. atem_control/routing.py +11 -0
  32. atem_control/sightings.py +14 -0
  33. atem_control/static/css/atem_connect.css +34 -0
  34. atem_control/static/css/atem_control.css +872 -0
  35. atem_control/static/css/theme.css +620 -0
  36. atem_control/static/fonts/poppins/OFL.txt +93 -0
  37. atem_control/static/fonts/poppins/Poppins-Bold.ttf +0 -0
  38. atem_control/static/fonts/poppins/Poppins-Italic.ttf +0 -0
  39. atem_control/static/fonts/poppins/Poppins-Medium.ttf +0 -0
  40. atem_control/static/fonts/poppins/Poppins-Regular.ttf +0 -0
  41. atem_control/static/fonts/poppins/Poppins-SemiBold.ttf +0 -0
  42. atem_control/static/icon.png +0 -0
  43. atem_control/static/js/atem_connect.js +403 -0
  44. atem_control/static/js/atem_control.js +4151 -0
  45. atem_control/static/js/atem_control_page.js +864 -0
  46. atem_control/static/js/atem_discovery.js +150 -0
  47. atem_control/static/js/atem_hyperdeck.js +405 -0
  48. atem_control/static/js/atem_profile.js +569 -0
  49. atem_control/static/js/atem_realtime_slider.js +248 -0
  50. atem_control/static/js/custom-scrollbar.js +228 -0
  51. atem_control/static/js/pp-select.js +156 -0
  52. atem_control/static/vendor/README.md +30 -0
  53. atem_control/static/vendor/alpine.min.js +5 -0
  54. atem_control/static/vendor/alpinejs-collapse-3.14.3.min.js +1 -0
  55. atem_control/static/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
  56. atem_control/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  57. atem_control/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  58. atem_control/static/vendor/webatem.css +1 -0
  59. atem_control/storage.py +23 -0
  60. atem_control/tally.py +141 -0
  61. atem_control/templates/connect.html +109 -0
  62. atem_control/templates/control/_atem_info.html +91 -0
  63. atem_control/templates/control/_fade_to_black.html +38 -0
  64. atem_control/templates/control/_fairlight.html +1378 -0
  65. atem_control/templates/control/_header.html +150 -0
  66. atem_control/templates/control/_hyperdeck_panel.html +180 -0
  67. atem_control/templates/control/_macros.html +41 -0
  68. atem_control/templates/control/_media_pool.html +360 -0
  69. atem_control/templates/control/_profile_modal.html +313 -0
  70. atem_control/templates/control/_settings_dsk.html +410 -0
  71. atem_control/templates/control/_settings_transition.html +854 -0
  72. atem_control/templates/control/_settings_usk.html +1366 -0
  73. atem_control/templates/control/_slider.html +63 -0
  74. atem_control/templates/control/_transition_controls.html +36 -0
  75. atem_control/templates/control/_transition_style.html +19 -0
  76. atem_control/templates/control/_usk_fly.html +131 -0
  77. atem_control/templates/control/_usk_keyframe.html +161 -0
  78. atem_control/templates/control/_usk_mask.html +123 -0
  79. atem_control/templates/control/_usk_slider.html +8 -0
  80. atem_control/templates/control.html +808 -0
  81. atem_control/templatetags/__init__.py +0 -0
  82. atem_control/templatetags/static_v.py +54 -0
  83. atem_control/uploader.py +800 -0
  84. atem_control/urls.py +32 -0
  85. webatem/__init__.py +0 -0
  86. webatem/__main__.py +4 -0
  87. webatem/asgi.py +28 -0
  88. webatem/context.py +6 -0
  89. webatem/launcher.py +309 -0
  90. webatem/settings.py +142 -0
  91. webatem/templates/base.html +208 -0
  92. webatem/urls.py +7 -0
  93. webatem/websocket.py +133 -0
  94. webatem-0.2.0.dist-info/METADATA +335 -0
  95. webatem-0.2.0.dist-info/RECORD +99 -0
  96. webatem-0.2.0.dist-info/WHEEL +5 -0
  97. webatem-0.2.0.dist-info/entry_points.txt +2 -0
  98. webatem-0.2.0.dist-info/licenses/LICENSE +27 -0
  99. webatem-0.2.0.dist-info/top_level.txt +2 -0
File without changes
@@ -0,0 +1,37 @@
1
+ """Lightweight operator-activity logging.
2
+
3
+ The monorepo this app was extracted from records these events in a DB-backed
4
+ cross-app audit table; standalone writes them to the application log instead.
5
+ Signature-compatible with the original ``record_activity`` /
6
+ ``arecord_activity`` so call sites read identically. Connect/disconnect
7
+ events additionally persist in the ``ATEMControlLog`` table (which also
8
+ drives the Connect page's "Recent ATEMs").
9
+ """
10
+ import logging
11
+
12
+ _log = logging.getLogger('atem_control.activity')
13
+
14
+
15
+ class ActivityLog:
16
+ """Constant namespace kept for call-site compatibility."""
17
+ FEATURE_ATEM_CONTROL = 'atem_control'
18
+ DEVICE_ATEM = 'atem'
19
+ DEVICE_HYPERDECK = 'hyperdeck'
20
+
21
+
22
+ def record_activity(*, feature='', device='', action='', user=None,
23
+ target='', target_name='', summary='', **extra):
24
+ """Write one operator-activity line to the application log.
25
+
26
+ ``user`` is vestigial — kept for call-site compatibility with the
27
+ original monorepo's audit log. This app has no authentication, so
28
+ callers always pass ``None`` and entries are logged without a user
29
+ (the ``-`` placeholder).
30
+ """
31
+ username = getattr(user, 'username', None) or str(user or '-')
32
+ _log.info("activity user=%s action=%s target=%s: %s",
33
+ username, action, target_name or target, summary)
34
+
35
+
36
+ async def arecord_activity(**kwargs):
37
+ record_activity(**kwargs)
atem_control/apps.py ADDED
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AtemControlConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'atem_control'
7
+
8
+ def ready(self):
9
+ # Register the Channels bridge with the media_pool sub-package's
10
+ # on_watcher_created hook. Must happen before any watcher.acquire()
11
+ # call — importing the module runs its top-level on_watcher_created()
12
+ # registration as a side effect.
13
+ from atem_control.media_pool import broadcast # noqa: F401
@@ -0,0 +1,9 @@
1
+ """``control`` sub-package — ATEM control WebSocket consumer + dispatch.
2
+
3
+ Sub-modules:
4
+ consumer.py — ``ATEMConsumer`` (Channels WebSocket consumer)
5
+ commands.py — WebSocket-verb → op dispatch table (``ops`` built by walking
6
+ the per-feature ``pyatem.messages.<feature>`` modules;
7
+ ``pyatem.operations`` was deleted 2026-05-14)
8
+ logging.py — ``ATEMConnectionLoggingMixin`` (connect/disconnect audit trail)
9
+ """
@@ -0,0 +1,194 @@
1
+ """ATEM control command → activity-log classification + human summary
2
+ (2026-07-08).
3
+
4
+ Every operator action on the control page flows through the consumer's
5
+ ``execute_command`` → this module decides how to log it:
6
+
7
+ * DISCRETE actions (cut, program/preview, toggles, selects, macros) are
8
+ logged immediately, one row each.
9
+ * CONTINUOUS actions (slider/drag controls — chroma, wipe position,
10
+ faders, DVE geometry…) fire ~20×/second during a drag; the consumer
11
+ debounces them by ``debounce_key`` and logs ONE settled row per
12
+ adjustment instead of the whole stream.
13
+
14
+ ``summarize`` turns the command + its args into a readable one-liner,
15
+ resolving source ids to their labels from mixerstate where relevant.
16
+ """
17
+ from atemwire.messages.input_video import input_label
18
+
19
+ # Args that identify WHICH control (not its value) — used to build the
20
+ # debounce key and the summary's instance prefix.
21
+ ID_ARGS = {'me', 'key_index', 'keyer', 'dsk', 'dsk_idx', 'generator',
22
+ 'strip', 'source_id', 'band', 'player', 'aux', 'mixer'}
23
+
24
+ # Continuous (drag-slider) classification. A command is continuous if it
25
+ # ends in one of these value suffixes (or is an explicit multi-param
26
+ # slider), UNLESS it's an explicit discrete override (a select/action that
27
+ # happens to share a suffix).
28
+ _CONTINUOUS_SUFFIXES = (
29
+ '_x', '_y', '_hue', '_saturation', '_luma', '_gain', '_clip', '_softness',
30
+ '_symmetry', '_width', '_rotation', '_top', '_bottom', '_left', '_right',
31
+ '_opacity', '_duration', '_trigger_point', '_pre_roll', '_volume',
32
+ '_input_gain', '_pan', '_makeup', '_direction', '_altitude', '_shadow',
33
+ '_brightness', '_contrast', '_red', '_green', '_blue', '_key_edge',
34
+ '_spill', '_flare_suppression', '_bevel_position', '_bevel_softness',
35
+ '_sample_position', '_sample_size', '_band', '_size_x', '_size_y',
36
+ '_foreground', '_background',
37
+ )
38
+ _CONTINUOUS_OVERRIDES = {
39
+ 'set_audio_compressor', 'set_audio_limiter', 'set_audio_expander',
40
+ 'set_transition_position', # the T-bar drag (CTPs stream)
41
+ }
42
+ _DISCRETE_OVERRIDES = {
43
+ 'set_stinger_clip', # selects a clip index, not a drag
44
+ 'set_stinger_source',
45
+ 'run_flying_key_infinite_direction', # an action
46
+ }
47
+
48
+
49
+ def is_continuous(command: str) -> bool:
50
+ if command in _DISCRETE_OVERRIDES:
51
+ return False
52
+ if command in _CONTINUOUS_OVERRIDES:
53
+ return True
54
+ return command.endswith(_CONTINUOUS_SUFFIXES)
55
+
56
+
57
+ def debounce_key(command: str, data: dict):
58
+ """Key a slider stream by its control identity (command + which
59
+ ME/keyer/strip/etc.), NOT its value — so USK1 hue and USK2 hue debounce
60
+ independently but successive ticks of the same slider coalesce."""
61
+ ident = tuple(sorted((k, data[k]) for k in data
62
+ if k in ID_ARGS and _scalar(data[k])))
63
+ return (command, ident)
64
+
65
+
66
+ def _scalar(v):
67
+ return isinstance(v, (str, int, float, bool)) or v is None
68
+
69
+
70
+ def _instance_prefix(data: dict) -> str:
71
+ """Readable 'which control' prefix, e.g. 'USK2 ', 'DSK1 ', 'ME2 '."""
72
+ parts = []
73
+ me = data.get('me')
74
+ if isinstance(me, int) and me > 0:
75
+ parts.append(f"ME{me + 1}")
76
+ if 'key_index' in data:
77
+ parts.append(f"USK{_int(data['key_index']) + 1}")
78
+ elif 'keyer' in data:
79
+ parts.append(f"USK{_int(data['keyer']) + 1}")
80
+ if 'dsk' in data:
81
+ parts.append(f"DSK{_int(data['dsk']) + 1}")
82
+ elif 'dsk_idx' in data:
83
+ parts.append(f"DSK{_int(data['dsk_idx']) + 1}")
84
+ if 'generator' in data:
85
+ parts.append(f"ColorGen{_int(data['generator']) + 1}")
86
+ if 'strip' in data:
87
+ parts.append(f"Strip {data['strip']}")
88
+ return (' '.join(parts) + ' ') if parts else ''
89
+
90
+
91
+ def _int(v, default=0):
92
+ try:
93
+ return int(v)
94
+ except (TypeError, ValueError):
95
+ return default
96
+
97
+
98
+ def _value_str(data: dict) -> str:
99
+ """The value part of the command — every non-identity scalar arg,
100
+ formatted compactly. Rounds floats; joins multiples with commas."""
101
+ bits = []
102
+ for k, v in data.items():
103
+ if k in ID_ARGS or not _scalar(v):
104
+ continue
105
+ if isinstance(v, float):
106
+ v = round(v, 2)
107
+ bits.append(str(v))
108
+ return ', '.join(bits)
109
+
110
+
111
+ def _humanize(command: str, data: dict) -> str:
112
+ """Generic fallback: 'set_usk_pattern_softness' + {keyer:0, softness:40}
113
+ → 'USK1 pattern softness → 40'."""
114
+ name = command
115
+ for pre in ('set_', 'toggle_', 'run_'):
116
+ if name.startswith(pre):
117
+ name = name[len(pre):]
118
+ break
119
+ prefix = _instance_prefix(data)
120
+ # Drop tokens the prefix already conveys, to avoid 'USK1 usk pattern …'.
121
+ words = [w for w in name.split('_')
122
+ if w not in ('usk', 'dsk', 'me', 'keyer', 'strip', 'generator')]
123
+ label = ' '.join(words) or name.replace('_', ' ')
124
+ value = _value_str(data)
125
+ text = f"{prefix}{label}"
126
+ if command.startswith('toggle_'):
127
+ text = f"Toggled {text}"
128
+ return f"{text} → {value}" if value else text.strip().capitalize()
129
+
130
+
131
+ def summarize(command: str, data: dict, mixerstate=None) -> str:
132
+ """Human one-liner for a control command. ``mixerstate`` (optional)
133
+ resolves source ids to labels for the source-picking commands."""
134
+ d = data or {}
135
+ me = d.get('me')
136
+ me_suffix = f" (ME{me + 1})" if isinstance(me, int) and me > 0 else ''
137
+
138
+ def src(sid):
139
+ if mixerstate is not None:
140
+ lbl = input_label(mixerstate, _int(sid))
141
+ name = lbl.get('long') or lbl.get('short')
142
+ if name:
143
+ return name
144
+ return f"input {sid}"
145
+
146
+ if command == 'cut':
147
+ return f"Cut{me_suffix}"
148
+ if command == 'auto':
149
+ return f"Auto transition{me_suffix}"
150
+ if command == 'set_transition_position':
151
+ return f"T-bar → {round(_int(d.get('position')) / 100)}%{me_suffix}"
152
+ if command == 'set_program':
153
+ return f"Program → {src(d.get('source'))}{me_suffix}"
154
+ if command == 'set_preview':
155
+ return f"Preview → {src(d.get('source'))}{me_suffix}"
156
+ if command == 'toggle_usk':
157
+ return f"Toggled USK{_int(d.get('key_index')) + 1} on air{me_suffix}"
158
+ if command == 'toggle_dsk':
159
+ return f"Toggled DSK{_int(d.get('dsk')) + 1} on air"
160
+ if command == 'dsk_auto':
161
+ return f"DSK{_int(d.get('dsk')) + 1} auto"
162
+ if command == 'fade_to_black':
163
+ return f"Fade to black{me_suffix}"
164
+ if command == 'toggle_transition_background':
165
+ return f"Toggled background in next transition{me_suffix}"
166
+ if command == 'toggle_transition_key':
167
+ return f"Toggled USK{_int(d.get('key_index')) + 1} in next transition{me_suffix}"
168
+ if command == 'set_transition_style':
169
+ return f"Transition style → {d.get('style')}{me_suffix}"
170
+ if command == 'set_aux_output':
171
+ aux = d.get('aux')
172
+ aux_lbl = f"Aux {aux + 1}" if isinstance(aux, int) else "Aux"
173
+ return f"{aux_lbl} → {src(d.get('source'))}"
174
+ if command == 'set_video_mode':
175
+ return f"Video mode → {d.get('mode', d.get('video_mode'))}"
176
+ if command == 'set_input_label':
177
+ lbl = d.get('long_name') or d.get('short_name') or ''
178
+ return f"Renamed input {d.get('source')} to '{lbl}'"
179
+ if command == 'execute_macro':
180
+ idx = d.get('index', d.get('macro'))
181
+ return f"Ran macro {idx + 1 if isinstance(idx, int) else idx}"
182
+ if command == 'set_media_player_still':
183
+ return (f"Media player {_int(d.get('player')) + 1} → still "
184
+ f"{_int(d.get('still', d.get('index'))) + 1}")
185
+ if command in ('set_wipe_fill_source', 'set_dve_fill_source',
186
+ 'set_usk_fill_source', 'set_dsk_fill_source',
187
+ 'set_dip_source', 'set_stinger_source', 'set_wipe_pattern'):
188
+ val = d.get('source') or d.get('fillSource') or d.get('pattern') \
189
+ or d.get('input_source')
190
+ if command.endswith('_source') and str(val).isdigit():
191
+ val = src(val)
192
+ return f"{_humanize(command, {k: v for k, v in d.items() if k in ID_ARGS})} → {val}"
193
+
194
+ return _humanize(command, d)