cuju-render 0.1.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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Video export and its jobs, the clean feed and the shared picture sizes."""
@@ -0,0 +1,542 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The clean feed of the renderer, a picture of fixed size for a streaming program such as OBS.
5
+
6
+ The page at `/feed` shows the Watch page with its broadcast graphics alone, at the size, scale, safe
7
+ area and background that its query names, and follows the source that the query names. This module
8
+ reads and writes that query as `web/js/export/feed-params.js` does, answers which match or live
9
+ stream a source names at the moment, for the page's `GET /api/feed/resolve`, lists the sources a
10
+ feed may follow, for `GET /api/feed`, and runs `cuju render feed`, which prints the addresses of
11
+ feeds for people, agents and scripts.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import math
19
+ import re
20
+ import sys
21
+ import urllib.parse
22
+ from collections.abc import Callable
23
+ from typing import Any
24
+
25
+ from cuju.errors import ArgumentError
26
+
27
+ from .._text import messages as _messages
28
+ from .._text.messages import MessageError
29
+ from . import sizes as _sizes
30
+
31
+ # The sources that a feed follows, as the page names them.
32
+ SOURCES = ('auto', 'live', 'latest', 'tournament', 'queue', 'recording')
33
+ # The sources that follow others, every source but a single recording.
34
+ FOLLOWING = ('auto', 'live', 'latest', 'tournament', 'queue')
35
+ CAMERAS = ('broadcast', 'tactical', 'goal', 'ball', 'owner')
36
+ ARENAS = ('stadium', 'beach', 'rooftop', 'space', 'neon')
37
+ HOURS = {'dawn': 6.5, 'day': 15.0, 'dusk': 19.0, 'night': 22.0}
38
+ QUALITIES = ('low', 'medium', 'high')
39
+ THEMES = ('dark', 'light')
40
+ LAYERS = ('full', 'graphics')
41
+ BACKGROUNDS = {
42
+ 'transparent': 'transparent',
43
+ 'black': '#000000',
44
+ 'green': '#00b140',
45
+ 'blue': '#0047bb',
46
+ 'magenta': '#ff00ff',
47
+ 'white': '#ffffff',
48
+ }
49
+ MAX_FPS = 60
50
+ # The options of a feed and their defaults, as `FEED_DEFAULTS` of the page.
51
+ DEFAULTS: dict[str, Any] = {
52
+ 'source': 'auto',
53
+ 'size': _sizes.DEFAULT_SIZE,
54
+ 'scale': 1.0,
55
+ 'safe': 'auto',
56
+ 'bg': 'black',
57
+ 'layer': 'full',
58
+ 'audio': False,
59
+ 'volume': 0.5,
60
+ 'fps': 0,
61
+ 'camera': 'broadcast',
62
+ 'loop': False,
63
+ 'broadcast': True,
64
+ 'show': True,
65
+ 'score': True,
66
+ 'captions': False,
67
+ 'slate': True,
68
+ 'quality': 'high',
69
+ 'theme': 'dark',
70
+ 'arena': None,
71
+ 'time': None,
72
+ 't': None,
73
+ }
74
+ _IDENTIFIER = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}')
75
+
76
+
77
+ def parse_source(text: Any) -> tuple[str, str]:
78
+ """Returns the kind of a source and the identifier, title or tournament it names.
79
+
80
+ A source is `auto`, `live`, `latest`, `tournament`, `queue`, a kind with a name after a colon,
81
+ such as `live:Evaluation` or `tournament:t-20261110-101500-3fa9`, or the identifier of a
82
+ recording, alone or as `recording:ID`.
83
+
84
+ Raises:
85
+ ArgumentError: If the text names no source.
86
+ """
87
+ value = str(text if text is not None else '').strip()
88
+ if not value:
89
+ return 'auto', ''
90
+ head, colon, rest = value.partition(':')
91
+ head, rest = head.lower(), rest.strip()
92
+ if head in SOURCES:
93
+ if head == 'recording' and not rest:
94
+ raise MessageError('server.feed.recording_name')
95
+ if head in ('auto', 'latest', 'queue') and colon:
96
+ raise MessageError('server.feed.nothing_after', source=head, value=repr(value))
97
+ return head, rest
98
+ if _IDENTIFIER.fullmatch(value):
99
+ return 'recording', value
100
+ raise MessageError('server.feed.source', sources=', '.join(SOURCES), value=repr(value))
101
+
102
+
103
+ def source_text(kind: str, name: str) -> str:
104
+ """Writes a source back as an address names it."""
105
+ return f'{kind}:{name}' if name else kind
106
+
107
+
108
+ def parse_background(text: Any) -> str:
109
+ """Returns the CSS color of a background, a name of `BACKGROUNDS` or a hex color."""
110
+ value = str(text).strip().lower()
111
+ if value in BACKGROUNDS:
112
+ return BACKGROUNDS[value]
113
+ digits = value.removeprefix('#')
114
+ if re.fullmatch(r'[0-9a-f]{3}|[0-9a-f]{6}', digits):
115
+ return '#' + (''.join(c * 2 for c in digits) if len(digits) == 3 else digits)
116
+ raise ArgumentError(
117
+ f'expected a background of {", ".join(BACKGROUNDS)} or a hex color, found {text!r}'
118
+ )
119
+
120
+
121
+ def _number(value: Any, name: str, low: float, high: float) -> float:
122
+ try:
123
+ number = float(value)
124
+ except (TypeError, ValueError):
125
+ number = math.nan
126
+ if isinstance(value, bool) or not low <= number <= high:
127
+ raise ArgumentError(f'expected {name} from {low:g} to {high:g}, found {value!r}')
128
+ return number
129
+
130
+
131
+ def _choice(value: Any, name: str, choices: tuple[str, ...]) -> str:
132
+ text = str(value).strip().lower()
133
+ if text not in choices:
134
+ raise ArgumentError(f'expected {name} of {", ".join(choices)}, found {value!r}')
135
+ return text
136
+
137
+
138
+ def check(**given: Any) -> dict[str, Any]:
139
+ """Returns the checked options of a feed, its defaults beside the options given.
140
+
141
+ The size is a preset or `WxH`, or the width and height given as `width` and `height`. The
142
+ time of day is a name of `HOURS` or an hour.
143
+
144
+ Raises:
145
+ ArgumentError: If an option is unknown or out of place.
146
+ """
147
+ width, height = given.pop('width', None), given.pop('height', None)
148
+ unknown = set(given) - set(DEFAULTS)
149
+ if unknown:
150
+ raise ArgumentError(f'expected options of {", ".join(DEFAULTS)}, found {sorted(unknown)}')
151
+ options = {**DEFAULTS, **{key: value for key, value in given.items() if value is not None}}
152
+ options['source'] = source_text(*parse_source(options['source']))
153
+ size = _sizes.parse_size(options['size'])
154
+ if width is not None or height is not None:
155
+ size = _sizes.check_size(
156
+ size[0] if width is None else width, size[1] if height is None else height
157
+ )
158
+ options['size'] = _sizes.size_name(*size)
159
+ options['scale'] = _sizes.parse_graphics(options['scale'])
160
+ options['safe'] = _sizes.parse_safe(options['safe'])
161
+ parse_background(options['bg'])
162
+ options['bg'] = str(options['bg']).strip().lower().removeprefix('#')
163
+ options['layer'] = _choice(options['layer'], 'a layer', LAYERS)
164
+ for flag in ('audio', 'loop', 'broadcast', 'show', 'score', 'captions', 'slate'):
165
+ if not isinstance(options[flag], bool):
166
+ raise ArgumentError(f'expected {flag} as true or false, found {options[flag]!r}')
167
+ options['volume'] = _number(options['volume'], 'a volume', 0, 1)
168
+ fps = options['fps']
169
+ if isinstance(fps, bool) or not isinstance(fps, int) or not 0 <= fps <= MAX_FPS:
170
+ raise ArgumentError(f'expected a frame rate from 0 to {MAX_FPS}, found {fps!r}')
171
+ options['camera'] = _choice(options['camera'], 'a camera', CAMERAS)
172
+ options['quality'] = _choice(options['quality'], 'a quality', QUALITIES)
173
+ options['theme'] = _choice(options['theme'], 'a theme', THEMES)
174
+ if options['arena'] is not None:
175
+ options['arena'] = _choice(options['arena'], 'an arena', ARENAS)
176
+ if options['time'] is not None:
177
+ hour = str(options['time']).strip().lower()
178
+ options['time'] = HOURS[hour] if hour in HOURS else _number(hour, 'an hour', 0, 24)
179
+ if options['t'] is not None:
180
+ options['t'] = _number(options['t'], 'a start', 0, 1e6)
181
+ return options
182
+
183
+
184
+ def _written(value: Any) -> str:
185
+ if isinstance(value, bool):
186
+ return '1' if value else '0'
187
+ if isinstance(value, float) and value.is_integer():
188
+ return str(int(value))
189
+ return str(value)
190
+
191
+
192
+ def query(**given: Any) -> str:
193
+ """Returns the query of a feed's address, the source and size always and every other option
194
+ that differs from its default, in the order of the page's `feedQuery`."""
195
+ options = check(**given)
196
+ pairs = []
197
+ for key, default in DEFAULTS.items():
198
+ value = options[key]
199
+ if key not in ('source', 'size') and (value is None or value == default):
200
+ continue
201
+ pairs.append((key, _written(value)))
202
+ return urllib.parse.urlencode(pairs, safe=':')
203
+
204
+
205
+ def address(base: str, **given: Any) -> str:
206
+ """Returns the whole address of a feed on the server at `base`, such as the server's URL."""
207
+ return f'{base.rstrip("/")}/feed?{query(**given)}'
208
+
209
+
210
+ # The answers of the server, the targets that a source names and the sources there are.
211
+
212
+
213
+ def _newest_live(sources: list[dict[str, Any]], name: str = '') -> dict[str, Any] | None:
214
+ ordered = sorted(sources, key=lambda source: str(source.get('id', '')), reverse=True)
215
+ if name:
216
+ exact = [source for source in ordered if source.get('id') == name]
217
+ if exact:
218
+ return exact[0]
219
+ folded = name.casefold()
220
+ ordered = [s for s in ordered if str(s.get('title') or '').casefold() == folded]
221
+ return ordered[0] if ordered else None
222
+
223
+
224
+ def _target(kind: str, identifier: str, entry: dict[str, Any], **extra: Any) -> dict[str, Any]:
225
+ """Returns a target, whose title comes from the summary or the source `entry`, with the
226
+ message of a title that cuju made beside its English text."""
227
+ return {'kind': kind, 'id': identifier, **_messages.named('title', _title(entry)), **extra}
228
+
229
+
230
+ def _title(entry: dict[str, Any] | None) -> dict[str, Any] | str:
231
+ """Returns the title of a summary or a live source, a label when it carries a valid one."""
232
+ entry = entry or {}
233
+ found = {
234
+ 'id': entry.get('title_id'),
235
+ 'text': str(entry.get('title') or ''),
236
+ 'values': entry.get('title_values'),
237
+ }
238
+ return found if _messages.valid_label(found) else found['text']
239
+
240
+
241
+ def _none(message_id: str, **values: Any) -> dict[str, Any]:
242
+ """Returns the answer for a source that names nothing now, with the message of what it waits
243
+ for, whose English text stays in `message`."""
244
+ label = _messages.label(message_id, **values)
245
+ return {'kind': 'none', 'id': '', 'title': '', **_messages.named('message', label)}
246
+
247
+
248
+ def _tournament_match(entry: dict[str, Any]) -> str | None:
249
+ """Returns the recording of the newest finished match of a tournament, or `None`."""
250
+ played = [
251
+ match['result']['match']
252
+ for match in entry.get('matches', [])
253
+ if isinstance(match.get('result'), dict) and isinstance(match['result'].get('match'), str)
254
+ ]
255
+ return max(played) if played else None
256
+
257
+
258
+ def resolve(
259
+ source: str,
260
+ live: Callable[[], list[dict[str, Any]]],
261
+ recordings: Callable[[], list[dict[str, Any]]],
262
+ recording: Callable[[str], dict[str, Any] | None],
263
+ requests: Callable[[], list[dict[str, Any]]],
264
+ tournaments: Callable[[], list[dict[str, Any]]],
265
+ tournament: Callable[[str], dict[str, Any] | None],
266
+ ) -> dict[str, Any]:
267
+ """Returns what a feed's source names now, a recording, a live source or none.
268
+
269
+ Each callable reads one part of the server, the live sources, the recordings newest first,
270
+ one recording's summary, the requests newest first, the tournaments newest first and one
271
+ tournament in full. The answer holds `kind`, `match`, `live` or `none`, the `id` and `title`
272
+ of the target, and for none a `message` that says what the feed waits for.
273
+
274
+ Raises:
275
+ ArgumentError: If the text names no source.
276
+ """
277
+ kind, name = parse_source(source)
278
+ if kind in ('auto', 'live'):
279
+ found = _newest_live(live(), name)
280
+ if found is not None:
281
+ return _target('live', found['id'], found)
282
+ if kind == 'live':
283
+ if name:
284
+ return _none('server.feed.no_live_named', name=name)
285
+ return _none('server.feed.no_live')
286
+ if kind in ('auto', 'latest'):
287
+ entries = recordings()
288
+ if entries:
289
+ return _target('match', entries[0]['id'], entries[0])
290
+ return _none('server.feed.no_recordings')
291
+ if kind == 'recording':
292
+ entry = recording(name)
293
+ if entry is None:
294
+ return _none('server.feed.no_recording', name=name)
295
+ return _target('match', name, entry)
296
+ if kind == 'queue':
297
+ done = [
298
+ entry
299
+ for entry in requests()
300
+ if entry.get('state') == 'done' and isinstance(entry.get('match'), str)
301
+ ]
302
+ if done:
303
+ newest = max(done, key=lambda entry: float(entry.get('finished') or 0))
304
+ # The recording's own title names the teams as the match did.
305
+ against = _messages.label(
306
+ 'server.names.against', left=newest.get('left', ''), right=newest.get('right', '')
307
+ )
308
+ entry = recording(newest['match']) or {
309
+ 'title': against['text'],
310
+ 'title_id': against['id'],
311
+ 'title_values': against['values'],
312
+ }
313
+ return _target('match', newest['match'], entry, request=newest['id'])
314
+ return _none('server.feed.no_request')
315
+ summaries = tournaments()
316
+ if name:
317
+ chosen = next((t for t in summaries if t.get('id') == name), None) or next(
318
+ (t for t in summaries if str(t.get('name') or '').casefold() == name.casefold()), None
319
+ )
320
+ if chosen is None:
321
+ return _none('server.feed.no_tournament_named', name=name)
322
+ elif summaries:
323
+ chosen = summaries[0]
324
+ else:
325
+ return _none('server.feed.no_tournament')
326
+ entry = tournament(chosen['id'])
327
+ match = _tournament_match(entry) if entry else None
328
+ if match is None:
329
+ if chosen.get('name'):
330
+ return _none('server.feed.tournament_waits', name=str(chosen['name']))
331
+ return _none('server.feed.tournament_waits_unnamed')
332
+ named = {'title': str(chosen.get('name') or '')}
333
+ return _target('match', match, named, tournament=chosen['id'])
334
+
335
+
336
+ def targets(
337
+ base: str,
338
+ live: list[dict[str, Any]],
339
+ tournaments: list[dict[str, Any]],
340
+ ) -> list[dict[str, Any]]:
341
+ """Returns the sources that a feed may follow now, each with its address on `base`."""
342
+ rows = [
343
+ {'source': kind, **_messages.named('label', _messages.label(f'server.feed.follow.{kind}'))}
344
+ for kind in ('auto', 'latest', 'live', 'queue', 'tournament')
345
+ ]
346
+ # A live source goes by its title, which follows it through its restarts, and a source without
347
+ # a title by its identifier. Sources of one title share a row, the newest of them.
348
+ named = set()
349
+ for source in sorted(live, key=lambda s: str(s.get('id', '')), reverse=True):
350
+ title = str(source.get('title') or '').strip()
351
+ name = title or str(source['id'])
352
+ if name.casefold() in named:
353
+ continue
354
+ named.add(name.casefold())
355
+ shown = _title(source) if title else name
356
+ label = _messages.label('server.feed.follow.live_named', name=shown)
357
+ rows.append({'source': f'live:{name}', **_messages.named('label', label)})
358
+ for entry in tournaments[:8]:
359
+ label = _messages.label(
360
+ 'server.feed.follow.tournament_named', name=str(entry.get('name') or entry['id'])
361
+ )
362
+ rows.append({'source': f'tournament:{entry["id"]}', **_messages.named('label', label)})
363
+ for row in rows:
364
+ row['url'] = address(base, source=row['source'])
365
+ return rows
366
+
367
+
368
+ # The command line.
369
+
370
+
371
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
372
+ """Adds the arguments of `cuju render feed` to a parser."""
373
+ parser.add_argument(
374
+ 'source',
375
+ nargs='?',
376
+ help='what the feed follows, auto, live, latest, tournament, queue, live:NAME, '
377
+ 'tournament:ID or a recording identifier, by default a list of every source',
378
+ )
379
+ parser.add_argument(
380
+ '--size', default=None, help='the picture size, as 1920x1080 or a preset, 1080p by default'
381
+ )
382
+ parser.add_argument('--width', type=int, default=None, help='the width in pixels')
383
+ parser.add_argument('--height', type=int, default=None, help='the height in pixels')
384
+ parser.add_argument('--scale', default=None, help='the scale of the graphics, 0.5 to 2')
385
+ parser.add_argument('--safe', default=None, help='the safe area, auto, none, social or percent')
386
+ parser.add_argument(
387
+ '--background',
388
+ dest='bg',
389
+ default=None,
390
+ help='transparent, black, green, blue, magenta, white or a hex color, black by default',
391
+ )
392
+ parser.add_argument(
393
+ '--graphics-only',
394
+ action='store_true',
395
+ help='the graphics alone over the background, without the picture',
396
+ )
397
+ parser.add_argument('--audio', action='store_true', help="the match's sound, silent by default")
398
+ parser.add_argument('--volume', type=float, default=None, help='the volume from 0 to 1')
399
+ parser.add_argument(
400
+ '--fps', type=int, default=None, help='the frame rate, 1 to 60, the display by default'
401
+ )
402
+ parser.add_argument('--camera', choices=CAMERAS, default=None, help='the camera')
403
+ parser.add_argument(
404
+ '--loop', action='store_true', help='play a recording again from its start at its end'
405
+ )
406
+ parser.add_argument(
407
+ '--plain',
408
+ action='store_true',
409
+ help='leave out the lineups, celebrations, replays, restarts and the show after the match',
410
+ )
411
+ parser.add_argument('--no-show', action='store_true', help='leave out the show after the match')
412
+ parser.add_argument('--no-score', action='store_true', help='hide the scoreboard and clock')
413
+ parser.add_argument('--captions', action='store_true', help='show captions of the events')
414
+ parser.add_argument(
415
+ '--no-slate', action='store_true', help='show the background alone while nothing plays'
416
+ )
417
+ parser.add_argument('--quality', choices=QUALITIES, default=None, help='high by default')
418
+ parser.add_argument('--theme', choices=THEMES, default=None, help='dark by default')
419
+ parser.add_argument('--arena', choices=ARENAS, default=None, help='the arena')
420
+ parser.add_argument('--time', default=None, help='dawn, day, dusk, night or an hour')
421
+ parser.add_argument('--start', type=float, default=None, help='the second a recording starts')
422
+ parser.add_argument('--json', action='store_true', help='print JSON, for scripts and agents')
423
+ parser.add_argument('--port', type=int, default=8780, help='the port when no server runs')
424
+
425
+
426
+ def options_from(namespace: argparse.Namespace) -> dict[str, Any]:
427
+ """Returns the checked options of a parsed command line, without its source."""
428
+ given = {
429
+ 'size': namespace.size,
430
+ 'width': namespace.width,
431
+ 'height': namespace.height,
432
+ 'scale': namespace.scale,
433
+ 'safe': namespace.safe,
434
+ 'bg': namespace.bg,
435
+ 'layer': 'graphics' if namespace.graphics_only else None,
436
+ 'audio': True if namespace.audio else None,
437
+ 'volume': namespace.volume,
438
+ 'fps': namespace.fps,
439
+ 'camera': namespace.camera,
440
+ 'loop': True if namespace.loop else None,
441
+ 'broadcast': False if namespace.plain else None,
442
+ 'show': False if namespace.no_show else None,
443
+ 'score': False if namespace.no_score else None,
444
+ 'captions': True if namespace.captions else None,
445
+ 'slate': False if namespace.no_slate else None,
446
+ 'quality': namespace.quality,
447
+ 'theme': namespace.theme,
448
+ 'arena': namespace.arena,
449
+ 'time': namespace.time,
450
+ 't': namespace.start,
451
+ }
452
+ return {key: value for key, value in given.items() if value is not None}
453
+
454
+
455
+ def _server_base(port: int) -> tuple[str, bool]:
456
+ """Returns the address of the running server of the home folder, or that of `port`."""
457
+ from .._http import launch as _launch
458
+
459
+ server = _launch.running()
460
+ if server is not None:
461
+ return server.url.split('#')[0].rstrip('/') + '/', True
462
+ return f'http://127.0.0.1:{port}/', False
463
+
464
+
465
+ def _listed(base: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
466
+ """Returns the live sources and tournaments that the running server lists."""
467
+ import urllib.request
468
+
469
+ def read(path: str) -> list[dict[str, Any]]:
470
+ try:
471
+ with urllib.request.urlopen(base + path, timeout=5) as response:
472
+ value = json.load(response)
473
+ except (OSError, ValueError):
474
+ return []
475
+ return value if isinstance(value, list) else []
476
+
477
+ return read('api/live'), read('api/tournaments')
478
+
479
+
480
+ def run(namespace: argparse.Namespace) -> int:
481
+ """Runs `cuju render feed`, which prints the address of a feed or of every source."""
482
+ if not 0 <= namespace.port <= 65535:
483
+ raise ArgumentError('expected a port from 0 to 65535')
484
+ options = options_from(namespace)
485
+ base, running = _server_base(namespace.port)
486
+ if namespace.source is not None:
487
+ url = address(base, source=namespace.source, **options)
488
+ _check_target(namespace.source, base, running)
489
+ rows = [{'source': source_text(*parse_source(namespace.source)), 'url': url}]
490
+ else:
491
+ check(**options)
492
+ live, cups = _listed(base) if running else ([], [])
493
+ rows = targets(base, live, cups)
494
+ for row in rows:
495
+ row['url'] = address(base, source=row['source'], **options)
496
+ if namespace.json:
497
+ print(json.dumps({'server': base, 'running': running, 'feeds': rows}, indent=2))
498
+ elif namespace.source is not None:
499
+ print(rows[0]['url'])
500
+ else:
501
+ for row in rows:
502
+ print(f'{row["url"]}\n {row["label"]}')
503
+ if not running:
504
+ print(
505
+ f'cuju render: no server of {_home_text()} answers, start it with cuju render serve '
506
+ f'--port {namespace.port} before adding the feed',
507
+ file=sys.stderr,
508
+ )
509
+ return 0
510
+
511
+
512
+ def _check_target(source: str, base: str, running: bool) -> None:
513
+ """Checks that a feed's recording is in the library and its tournament on the server.
514
+
515
+ A live source may start later, so a name of one passes. A recording must be in the library,
516
+ and a named tournament must be one of the running server.
517
+
518
+ Raises:
519
+ ArgumentError: If the recording or the tournament is unknown.
520
+ """
521
+ from .._store import library as _library
522
+
523
+ kind, name = parse_source(source)
524
+ if kind == 'recording' and _library.path_of(name) is None:
525
+ raise ArgumentError(
526
+ f'expected a recording of the library, found none named {name}, the Recordings page '
527
+ 'and cuju render search list them'
528
+ )
529
+ if kind == 'tournament' and name and running:
530
+ cups = _listed(base)[1]
531
+ folded = name.casefold()
532
+ if not any(
533
+ entry.get('id') == name or str(entry.get('name') or '').casefold() == folded
534
+ for entry in cups
535
+ ):
536
+ raise ArgumentError(f'expected a tournament of the server, found none named {name}')
537
+
538
+
539
+ def _home_text() -> str:
540
+ from .._store import home as _home
541
+
542
+ return str(_home.home())