startlive 1.2.1__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 (185) hide show
  1. StartLive.py +142 -0
  2. resources/dark-home.svg +9 -0
  3. resources/dark-log.svg +9 -0
  4. resources/dark-menu.svg +7 -0
  5. resources/dark-settings.svg +9 -0
  6. resources/dark-theme.svg +9 -0
  7. resources/icon_left.ico +0 -0
  8. resources/icon_left_macOS.ico +0 -0
  9. resources/light-home.svg +3 -0
  10. resources/light-log.svg +3 -0
  11. resources/light-menu.svg +3 -0
  12. resources/light-settings.svg +3 -0
  13. resources/light-theme.svg +3 -0
  14. resources/version.json +28 -0
  15. src/PySide/classes/__init__.py +6 -0
  16. src/PySide/classes/clickable_label.py +12 -0
  17. src/PySide/classes/completion_combo.py +32 -0
  18. src/PySide/classes/error_center.py +102 -0
  19. src/PySide/classes/focus_aware_password_edit.py +17 -0
  20. src/PySide/classes/focus_placeholder_line_edit.py +20 -0
  21. src/PySide/classes/single_instance_window.py +48 -0
  22. src/PySide/interface_adapters/__init__.py +1 -0
  23. src/PySide/interface_adapters/announce/__init__.py +2 -0
  24. src/PySide/interface_adapters/announce/announce_update_presenter.py +14 -0
  25. src/PySide/interface_adapters/announce/fetch_announce_presenter.py +20 -0
  26. src/PySide/interface_adapters/area/__init__.py +3 -0
  27. src/PySide/interface_adapters/area/area_update_presenter.py +28 -0
  28. src/PySide/interface_adapters/area/fetch_area_presenter.py +16 -0
  29. src/PySide/interface_adapters/area/fetch_recent_area_presenter.py +16 -0
  30. src/PySide/interface_adapters/const/__init__.py +2 -0
  31. src/PySide/interface_adapters/const/const_update_presenter.py +19 -0
  32. src/PySide/interface_adapters/const/version_checker_presenter.py +16 -0
  33. src/PySide/interface_adapters/cover/__init__.py +3 -0
  34. src/PySide/interface_adapters/cover/cover_state_presenter.py +14 -0
  35. src/PySide/interface_adapters/cover/cover_upload_presenter.py +22 -0
  36. src/PySide/interface_adapters/cover/fetch_cover_presenter.py +20 -0
  37. src/PySide/interface_adapters/credentials/__init__.py +1 -0
  38. src/PySide/interface_adapters/credentials/credential_manager_presenter.py +47 -0
  39. src/PySide/interface_adapters/face_auth/__init__.py +2 -0
  40. src/PySide/interface_adapters/face_auth/face_auth_presenter.py +20 -0
  41. src/PySide/interface_adapters/face_auth/face_captcha_presenter.py +19 -0
  42. src/PySide/interface_adapters/gui_dispatcher.py +29 -0
  43. src/PySide/interface_adapters/gui_presenter.py +20 -0
  44. src/PySide/interface_adapters/live/__init__.py +2 -0
  45. src/PySide/interface_adapters/live/start_live_presenter.py +63 -0
  46. src/PySide/interface_adapters/live/stop_live_presenter.py +19 -0
  47. src/PySide/interface_adapters/live_delay/__init__.py +2 -0
  48. src/PySide/interface_adapters/live_delay/fetch_shift_presenter.py +19 -0
  49. src/PySide/interface_adapters/live_delay/shift_update_presenter.py +16 -0
  50. src/PySide/interface_adapters/login/__init__.py +3 -0
  51. src/PySide/interface_adapters/login/buvid_ticket_presenter.py +14 -0
  52. src/PySide/interface_adapters/login/fetch_login_presenter.py +51 -0
  53. src/PySide/interface_adapters/login/fetch_qr_presenter.py +15 -0
  54. src/PySide/interface_adapters/obs_ws/__init__.py +3 -0
  55. src/PySide/interface_adapters/obs_ws/obs_connector_presenter.py +39 -0
  56. src/PySide/interface_adapters/obs_ws/obs_daemon_presenter.py +13 -0
  57. src/PySide/interface_adapters/obs_ws/wait_obs_presenter.py +17 -0
  58. src/PySide/interface_adapters/pre_live/__init__.py +1 -0
  59. src/PySide/interface_adapters/pre_live/fetch_pre_live_presenter.py +44 -0
  60. src/PySide/interface_adapters/title/__init__.py +2 -0
  61. src/PySide/interface_adapters/title/recent_title_presenter.py +22 -0
  62. src/PySide/interface_adapters/title/title_update_presenter.py +17 -0
  63. src/PySide/log/__init__.py +22 -0
  64. src/PySide/log/handler/__init__.py +1 -0
  65. src/PySide/log/handler/signal_log.py +16 -0
  66. src/PySide/states/__init__.py +6 -0
  67. src/PySide/states/http_signal.py +7 -0
  68. src/PySide/states/log_state.py +5 -0
  69. src/PySide/states/login_state.py +12 -0
  70. src/PySide/states/obs_btn_state.py +7 -0
  71. src/PySide/states/restart_state.py +5 -0
  72. src/PySide/states/stream_state.py +6 -0
  73. src/PySide/updater/__init__.py +1 -0
  74. src/PySide/updater/update_controller.py +79 -0
  75. src/PySide/updater/update_worker.py +51 -0
  76. src/PySide/web_server/__init__.py +64 -0
  77. src/PySide/widgets/__init__.py +6 -0
  78. src/PySide/widgets/crop_label.py +556 -0
  79. src/PySide/widgets/log_viewer.py +39 -0
  80. src/PySide/widgets/recent_area.py +79 -0
  81. src/PySide/widgets/settings.py +321 -0
  82. src/PySide/widgets/side_bar_frame.py +131 -0
  83. src/PySide/widgets/sl_menu_bar.py +192 -0
  84. src/PySide/window/__init__.py +6 -0
  85. src/PySide/window/area_picker.py +333 -0
  86. src/PySide/window/cover_crop.py +90 -0
  87. src/PySide/window/face_qr.py +27 -0
  88. src/PySide/window/main_window.py +935 -0
  89. src/PySide/window/settings_page.py +244 -0
  90. src/PySide/window/stream_config.py +456 -0
  91. src/core/app_state/__init__.py +227 -0
  92. src/core/app_state/app_state_base.py +126 -0
  93. src/core/cache/__init__.py +52 -0
  94. src/core/constant/__init__.py +528 -0
  95. src/core/constant/_version.py +1 -0
  96. src/core/credentials/__init__.py +17 -0
  97. src/core/credentials/store.py +311 -0
  98. src/core/exceptions/AnnounceUpdateError.py +9 -0
  99. src/core/exceptions/AreaUpdateError.py +9 -0
  100. src/core/exceptions/CoverUploadError.py +9 -0
  101. src/core/exceptions/CredentialDuplicatedError.py +9 -0
  102. src/core/exceptions/CredentialExpiredError.py +9 -0
  103. src/core/exceptions/CredentialIndexCorruptedError.py +2 -0
  104. src/core/exceptions/CredentialRecordCorruptedError.py +12 -0
  105. src/core/exceptions/CredentialTransactionError.py +20 -0
  106. src/core/exceptions/CredentialValidationError.py +2 -0
  107. src/core/exceptions/LoginError.py +9 -0
  108. src/core/exceptions/RoomStatusError.py +9 -0
  109. src/core/exceptions/StartLiveError.py +9 -0
  110. src/core/exceptions/StopLiveError.py +9 -0
  111. src/core/exceptions/TaskCancelled.py +2 -0
  112. src/core/exceptions/TitleStatusError.py +8 -0
  113. src/core/exceptions/TitleUpdateError.py +9 -0
  114. src/core/exceptions/WorkerException.py +11 -0
  115. src/core/exceptions/__init__.py +16 -0
  116. src/core/log/__init__.py +44 -0
  117. src/core/log/formatter/__init__.py +1 -0
  118. src/core/log/formatter/thread_class.py +73 -0
  119. src/core/runtime.py +4 -0
  120. src/core/sign/__init__.py +5 -0
  121. src/core/sign/app_sign.py +80 -0
  122. src/core/sign/bili_ticket.py +19 -0
  123. src/core/sign/captcha_codec.py +300 -0
  124. src/core/sign/gen_buvid.py +6 -0
  125. src/core/sign/gen_dm_track.py +46 -0
  126. src/core/workers/__init__.py +2 -0
  127. src/core/workers/announce/__init__.py +2 -0
  128. src/core/workers/announce/announce_update.py +39 -0
  129. src/core/workers/announce/fetch_announce.py +28 -0
  130. src/core/workers/area/__init__.py +3 -0
  131. src/core/workers/area/area_update.py +39 -0
  132. src/core/workers/area/fetch_area.py +34 -0
  133. src/core/workers/area/fetch_recent_area.py +29 -0
  134. src/core/workers/base/BaseWorker.py +123 -0
  135. src/core/workers/base/CancellationToken.py +43 -0
  136. src/core/workers/base/LongLiveWorker.py +67 -0
  137. src/core/workers/base/Presenter.py +15 -0
  138. src/core/workers/base/__init__.py +4 -0
  139. src/core/workers/const/__init__.py +2 -0
  140. src/core/workers/const/const_update.py +55 -0
  141. src/core/workers/const/version_checker.py +32 -0
  142. src/core/workers/cover/__init__.py +3 -0
  143. src/core/workers/cover/cover_state_update.py +42 -0
  144. src/core/workers/cover/cover_upload.py +69 -0
  145. src/core/workers/cover/fetch_cover.py +20 -0
  146. src/core/workers/credentials/__init__.py +1 -0
  147. src/core/workers/credentials/credential_manager.py +188 -0
  148. src/core/workers/dispatcher.py +9 -0
  149. src/core/workers/face_auth/__init__.py +3 -0
  150. src/core/workers/face_auth/face_auth.py +67 -0
  151. src/core/workers/face_auth/face_captcha.py +48 -0
  152. src/core/workers/face_auth/report_face.py +37 -0
  153. src/core/workers/live/__init__.py +2 -0
  154. src/core/workers/live/start_live.py +140 -0
  155. src/core/workers/live/stop_live.py +47 -0
  156. src/core/workers/live_delay/__init__.py +2 -0
  157. src/core/workers/live_delay/fetch_time_shift.py +30 -0
  158. src/core/workers/live_delay/time_shift_update.py +32 -0
  159. src/core/workers/login/__init__.py +3 -0
  160. src/core/workers/login/buvid_ticket.py +54 -0
  161. src/core/workers/login/fetch_login.py +62 -0
  162. src/core/workers/login/fetch_qr.py +36 -0
  163. src/core/workers/obs_ws/__init__.py +3 -0
  164. src/core/workers/obs_ws/obs_connector.py +37 -0
  165. src/core/workers/obs_ws/obs_daemon.py +39 -0
  166. src/core/workers/obs_ws/wait_obs.py +17 -0
  167. src/core/workers/pre_live/__init__.py +2 -0
  168. src/core/workers/pre_live/fetch_pre_live.py +68 -0
  169. src/core/workers/pre_live/fetch_room_status.py +25 -0
  170. src/core/workers/room/__init__.py +2 -0
  171. src/core/workers/room/live_report.py +43 -0
  172. src/core/workers/room/room_content_audit.py +39 -0
  173. src/core/workers/title/__init__.py +2 -0
  174. src/core/workers/title/recent_title.py +26 -0
  175. src/core/workers/title/title_update.py +48 -0
  176. src/core/workers/usernames/__init__.py +1 -0
  177. src/core/workers/usernames/fetch_usernames.py +57 -0
  178. src/core/workers/worker_manager.py +122 -0
  179. startlive-1.2.1.dist-info/METADATA +154 -0
  180. startlive-1.2.1.dist-info/RECORD +185 -0
  181. startlive-1.2.1.dist-info/WHEEL +5 -0
  182. startlive-1.2.1.dist-info/entry_points.txt +2 -0
  183. startlive-1.2.1.dist-info/licenses/LICENSE +674 -0
  184. startlive-1.2.1.dist-info/licenses/LICENSE-ARTWORK +8 -0
  185. startlive-1.2.1.dist-info/top_level.txt +3 -0
StartLive.py ADDED
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import sys
4
+ from argparse import ArgumentParser, SUPPRESS
5
+ from pathlib import Path
6
+ from platform import system
7
+
8
+ _velopack_first_run = False
9
+
10
+
11
+ def _on_velopack_first_run(_version) -> None:
12
+ global _velopack_first_run
13
+ _velopack_first_run = True
14
+
15
+
16
+ def _run_velopack_hooks() -> None:
17
+ """Run installer hooks on platforms managed by Velopack."""
18
+ if system() == "Linux":
19
+ return
20
+
21
+ from velopack import App
22
+
23
+ App().on_first_run(_on_velopack_first_run).run()
24
+
25
+
26
+ def main() -> int:
27
+ from src.core.constant import VERSION
28
+
29
+ parser = ArgumentParser(prog="startlive")
30
+ parser.add_argument("--version", action="version",
31
+ version=f"StartLive {VERSION}")
32
+
33
+ parser.add_argument(
34
+ "--web.host",
35
+ dest="web_host",
36
+ default=None,
37
+ help="Host address for the web server",
38
+ )
39
+ parser.add_argument(
40
+ "--web.port",
41
+ dest="web_port",
42
+ type=int,
43
+ default=None,
44
+ help="Port for the web server",
45
+ )
46
+
47
+ # 兼容旧 Squirrel 启动参数。
48
+ parser.add_argument(
49
+ "--squirrel-firstrun",
50
+ dest="squirrel_first_run",
51
+ action="store_true",
52
+ help=SUPPRESS,
53
+ )
54
+
55
+ parser.add_argument(
56
+ "--noupdate",
57
+ dest="no_update",
58
+ action="store_true",
59
+ )
60
+
61
+ args, qt_args = parser.parse_known_args()
62
+
63
+ # 将较重的应用模块放在 Velopack 启动处理之后导入,
64
+ # 可以避免安装/更新钩子执行时初始化完整 UI。
65
+ from PySide6.QtGui import QFont, QIcon
66
+ from PySide6.QtWidgets import QApplication
67
+ from qdarktheme import enable_hi_dpi
68
+
69
+ from src.PySide.classes import ErrorCenter, install_exception_handlers
70
+ from src.PySide.window import MainWindow
71
+ from src.core import app_state
72
+
73
+ if MainWindow.is_another_instance_running():
74
+ return 0
75
+
76
+ if system() == "Windows":
77
+ font_size = 9
78
+ icon_file = "icon_left.ico"
79
+ else:
80
+ font_size = 12
81
+ icon_file = "icon_left_macOS.ico"
82
+
83
+ first_run = _velopack_first_run or args.squirrel_first_run
84
+
85
+ enable_hi_dpi()
86
+ app = QApplication([sys.argv[0], *qt_args])
87
+ # %LocalAppData%/StartLive in velopack and squirrel.windows
88
+ base_path = Path(__file__).resolve().parent
89
+ app.setWindowIcon(
90
+ QIcon(str(base_path / "resources" / icon_file))
91
+ )
92
+
93
+ if (
94
+ (font := app_state.app_settings["custom_font"])
95
+ and (custom_font := QFont()).fromString(font)
96
+ ):
97
+ app.setFont(custom_font)
98
+ else:
99
+ app.setFont(
100
+ QFont(
101
+ "Open Sans,.AppleSystemUIFont,Helvetica,"
102
+ "Arial,MS Shell Dlg,sans-serif",
103
+ font_size,
104
+ )
105
+ )
106
+
107
+ window = MainWindow(
108
+ args.web_host,
109
+ args.web_port,
110
+ first_run,
111
+ args.no_update,
112
+ base_path=base_path,
113
+ )
114
+ error_center = ErrorCenter()
115
+ install_exception_handlers(error_center)
116
+
117
+ window.apply_color_scheme(
118
+ app.styleHints().colorScheme()
119
+ )
120
+ app.styleHints().colorSchemeChanged.connect(
121
+ window.apply_color_scheme
122
+ )
123
+
124
+ window.show()
125
+ return app.exec()
126
+
127
+
128
+ def cli() -> int:
129
+ """Entry point for installations managed by pip or uv tool."""
130
+ from src.core import cache, runtime
131
+
132
+ runtime.package_managed = True
133
+ cache._cache_dir.clear()
134
+ return main()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ # 必须在单实例检查、Qt 初始化和其他正常启动逻辑之前运行。
139
+ # 安装、更新或卸载期间,Velopack 可能在这里直接终止进程。
140
+ _run_velopack_hooks()
141
+
142
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0"?>
2
+ <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"
3
+ >
4
+ <g class="layer">
5
+ <title>Layer 1</title>
6
+ <path d="m12,5.69l5,4.5l0,7.81l-2,0l0,-6l-6,0l0,6l-2,0l0,-7.81l5,-4.5m0,-2.69l-10,9l3,0l0,8l6,0l0,-6l2,0l0,6l6,0l0,-8l3,0"
7
+ fill="#ffffff" id="svg_1"/>
8
+ </g>
9
+ </svg>
resources/dark-log.svg ADDED
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0"?>
2
+ <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"
3
+ >
4
+ <g class="layer">
5
+ <title>Layer 1</title>
6
+ <path d="m9,3c-3.31,0 -5.86,2.69 -6,6l0,12l9,0l0,-7.54c1.1,0.99 2.5,1.54 4,1.54c3.31,0 6,-2.69 6,-6c0,-3.31 -2.69,-6 -6,-6l-7,0m0,2l2.54,0c-0.99,1.1 -1.54,2.5 -1.54,4l0,3l-1,0l0,1l1,0l0,6l-5,0l0,-6l1,0l0,-1l-1,0l0,-3c0,-2.21 1.79,-4 4,-4m7,0c2.21,0 4,1.79 4,4c0,2.21 -1.79,4 -4,4c-2.21,0 -4,-1.79 -4,-4c0,-2.21 1.79,-4 4,-4m0,2.25c-0.97,0 -1.75,0.78 -1.75,1.75c0,0.97 0.78,1.75 1.75,1.75c0.97,0 1.75,-0.78 1.75,-1.75c0,-0.97 -0.78,-1.75 -1.75,-1.75m-9,4.75l0,1l1,0l0,-1l-1,0z"
7
+ fill="#ffffff" id="svg_1"/>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0"?>
2
+ <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
3
+ <g class="layer">
4
+ <title>Layer 1</title>
5
+ <path d="m3,6l18,0l0,2l-18,0l0,-2m0,5l18,0l0,2l-18,0l0,-2m0,5l18,0l0,2l-18,0l0,-2z" fill="#ffffff" id="svg_1"/>
6
+ </g>
7
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0"?>
2
+ <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"
3
+ >
4
+ <g class="layer">
5
+ <title>Layer 1</title>
6
+ <path d="m12,8a4,4 0 0 1 4,4a4,4 0 0 1 -4,4a4,4 0 0 1 -4,-4a4,4 0 0 1 4,-4m0,2a2,2 0 0 0 -2,2a2,2 0 0 0 2,2a2,2 0 0 0 2,-2a2,2 0 0 0 -2,-2m-2,12c-0.25,0 -0.46,-0.18 -0.5,-0.42l-0.37,-2.65c-0.63,-0.25 -1.17,-0.59 -1.69,-0.99l-2.49,1.01c-0.22,0.08 -0.49,0 -0.61,-0.22l-2,-3.46c-0.13,-0.22 -0.07,-0.49 0.12,-0.64l2.11,-1.66l-0.07,-0.97l0.07,-1l-2.11,-1.63c-0.19,-0.15 -0.25,-0.42 -0.12,-0.64l2,-3.46c0.12,-0.22 0.39,-0.31 0.61,-0.22l2.49,1c0.52,-0.39 1.06,-0.73 1.69,-0.98l0.37,-2.65c0.04,-0.24 0.25,-0.42 0.5,-0.42l4,0c0.25,0 0.46,0.18 0.5,0.42l0.37,2.65c0.63,0.25 1.17,0.59 1.69,0.98l2.49,-1c0.22,-0.09 0.49,0 0.61,0.22l2,3.46c0.13,0.22 0.07,0.49 -0.12,0.64l-2.11,1.63l0.07,1l-0.07,1l2.11,1.63c0.19,0.15 0.25,0.42 0.12,0.64l-2,3.46c-0.12,0.22 -0.39,0.31 -0.61,0.22l-2.49,-1c-0.52,0.39 -1.06,0.73 -1.69,0.98l-0.37,2.65c-0.04,0.24 -0.25,0.42 -0.5,0.42l-4,0m1.25,-18l-0.37,2.61c-1.2,0.25 -2.26,0.89 -3.03,1.78l-2.41,-1.04l-0.75,1.3l2.11,1.55c-0.4,1.17 -0.4,2.44 0,3.6l-2.12,1.56l0.75,1.3l2.43,-1.04c0.77,0.88 1.82,1.52 3.01,1.76l0.37,2.62l1.52,0l0.37,-2.61c1.19,-0.25 2.24,-0.89 3.01,-1.77l2.43,1.04l0.75,-1.3l-2.12,-1.55c0.4,-1.17 0.4,-2.44 0,-3.61l2.11,-1.55l-0.75,-1.3l-2.41,1.04c-0.77,-0.89 -1.83,-1.53 -3.03,-1.77l-0.37,-2.62l-1.5,0z"
7
+ fill="#ffffff" id="svg_1"/>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0"?>
2
+ <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"
3
+ >
4
+ <g class="layer">
5
+ <title>Layer 1</title>
6
+ <path d="m7.5,2c-1.79,1.15 -3,3.18 -3,5.5c0,2.32 1.21,4.35 3.03,5.5c-3.07,0 -5.53,-2.46 -5.53,-5.5a5.5,5.5 0 0 1 5.5,-5.5m11.57,1.5l1.43,1.43l-15.57,15.57l-1.43,-1.43l15.57,-15.57m-6.18,2.43l-1.48,-0.93l-1.44,1l0.42,-1.7l-1.39,-1.06l1.75,-0.12l0.58,-1.65l0.67,1.63l1.73,0.03l-1.35,1.13l0.51,1.67m-3.3,3.61l-1.16,-0.73l-1.12,0.78l0.34,-1.32l-1.09,-0.83l1.36,-0.09l0.45,-1.29l0.51,1.27l1.36,0.03l-1.05,0.87l0.4,1.31m9.41,3.96a5.5,5.5 0 0 1 -5.5,5.5c-1.22,0 -2.35,-0.4 -3.26,-1.07l7.69,-7.69c0.67,0.91 1.07,2.04 1.07,3.26m-4.4,6.58l2.77,-1.15l-0.24,3.35l-2.53,-2.2m4.33,-2.7l1.15,-2.77l2.2,2.54l-3.35,0.23m1.15,-4.96l-1.14,-2.78l3.34,0.24l-2.2,2.54m-10.45,6.51l2.77,1.15l-2.53,2.19l-0.24,-3.34z"
7
+ fill="#ffffff" id="svg_1"/>
8
+ </g>
9
+ </svg>
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M12 5.69L17 10.19V18H15V12H9V18H7V10.19L12 5.69M12 3L2 12H5V20H11V14H13V20H19V12H22"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M9 3C5.69 3 3.14 5.69 3 9V21H12V13.46C13.1 14.45 14.5 15 16 15C19.31 15 22 12.31 22 9C22 5.69 19.31 3 16 3H9M9 5H11.54C10.55 6.1 10 7.5 10 9V12H9V13H10V19H5V13H6V12H5V9C5 6.79 6.79 5 9 5M16 5C18.21 5 20 6.79 20 9C20 11.21 18.21 13 16 13C13.79 13 12 11.21 12 9C12 6.79 13.79 5 16 5M16 7.25C15.03 7.25 14.25 8.03 14.25 9C14.25 9.97 15.03 10.75 16 10.75C16.97 10.75 17.75 9.97 17.75 9C17.75 8.03 16.97 7.25 16 7.25M7 12V13H8V12H7Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <path d="M7.5,2C5.71,3.15 4.5,5.18 4.5,7.5C4.5,9.82 5.71,11.85 7.53,13C4.46,13 2,10.54 2,7.5A5.5,5.5 0 0,1 7.5,2M19.07,3.5L20.5,4.93L4.93,20.5L3.5,19.07L19.07,3.5M12.89,5.93L11.41,5L9.97,6L10.39,4.3L9,3.24L10.75,3.12L11.33,1.47L12,3.1L13.73,3.13L12.38,4.26L12.89,5.93M9.59,9.54L8.43,8.81L7.31,9.59L7.65,8.27L6.56,7.44L7.92,7.35L8.37,6.06L8.88,7.33L10.24,7.36L9.19,8.23L9.59,9.54M19,13.5A5.5,5.5 0 0,1 13.5,19C12.28,19 11.15,18.6 10.24,17.93L17.93,10.24C18.6,11.15 19,12.28 19,13.5M14.6,20.08L17.37,18.93L17.13,22.28L14.6,20.08M18.93,17.38L20.08,14.61L22.28,17.15L18.93,17.38M20.08,12.42L18.94,9.64L22.28,9.88L20.08,12.42M9.63,18.93L12.4,20.08L9.87,22.27L9.63,18.93Z"/>
3
+ </svg>
resources/version.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "ak": "aae92bc66f3edfab",
3
+ "as": "af125a0d5279fd576c1b4418a3e8276d",
4
+ "b": "11081",
5
+ "v": "8.7.0.11081",
6
+ "hw": {
7
+ "Accept": "*/*",
8
+ "Accept-Encoding": "gzip, deflate",
9
+ "Accept-Language": "zh-CN,zh;q=0.9",
10
+ "Connection": "keep-alive",
11
+ "Origin": "https://live.bilibili.com",
12
+ "Referer": "https://live.bilibili.com/",
13
+ "sec-ch-ua": "\"Chromium\";v=\"105\"",
14
+ "sec-ch-ua-mobile": "?0",
15
+ "sec-ch-ua-platform": "\"Windows\"",
16
+ "Sec-Fetch-Site": "same-site",
17
+ "Sec-Fetch-Mode": "cors",
18
+ "Sec-Fetch-Dest": "empty",
19
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 pc_app/livehime build/11081"
20
+ },
21
+ "ha": {
22
+ "Accept-Encoding": "gzip,deflate",
23
+ "Connection": "keep-alive",
24
+ "User-Agent": "LiveHime/8.7.0.11081 os/Windows pc_app/livehime build/11081 osVer/10.0_x86_64"
25
+ },
26
+ "start_ac": true,
27
+ "stop_ac": false
28
+ }
@@ -0,0 +1,6 @@
1
+ from .clickable_label import ClickableLabel
2
+ from .completion_combo import CompletionComboBox
3
+ from .error_center import ErrorCenter, install_exception_handlers
4
+ from .focus_aware_password_edit import FocusAwareLineEdit
5
+ from .focus_placeholder_line_edit import FocusPlaceholderLineEdit
6
+ from .single_instance_window import SingleInstanceWindow
@@ -0,0 +1,12 @@
1
+ from PySide6.QtCore import Signal
2
+ from PySide6.QtWidgets import QLabel
3
+
4
+
5
+ class ClickableLabel(QLabel):
6
+ clicked = Signal()
7
+
8
+ def __init__(self, parent=None):
9
+ super().__init__(parent)
10
+
11
+ def mousePressEvent(self, event):
12
+ self.clicked.emit()
@@ -0,0 +1,32 @@
1
+ from collections.abc import Sequence
2
+
3
+ from PySide6.QtCore import QStringListModel
4
+ from PySide6.QtWidgets import QComboBox, QCompleter
5
+
6
+
7
+ class CompletionComboBox(QComboBox):
8
+ def __init__(self, items: list[str], parent=None):
9
+ super().__init__(parent)
10
+ self.setEditable(True)
11
+ self.items = items.copy()
12
+ self.model = QStringListModel(items)
13
+ self.setModel(self.model)
14
+ self.completer = QCompleter(items, self)
15
+ self.setCompleter(self.completer)
16
+ self.lineEdit().textEdited.connect(self.update_completer)
17
+ super().addItems(items)
18
+
19
+ def addItems(self, texts: Sequence[str], /) -> None:
20
+ super().addItems(self.items)
21
+ self.items.extend(texts)
22
+ self.model.setStringList(self.items)
23
+
24
+ def clear(self):
25
+ self.items.clear()
26
+ self.model.setStringList([])
27
+ super().clear()
28
+
29
+ def update_completer(self, text):
30
+ filtered_items = [item for item in self.items
31
+ if text.lower() in item.lower()]
32
+ self.completer.model().setStringList(filtered_items)
@@ -0,0 +1,102 @@
1
+ import sys
2
+ import threading
3
+ import traceback
4
+
5
+ from PySide6.QtCore import QObject, Signal, Slot
6
+ from PySide6.QtWidgets import QMessageBox
7
+
8
+ from src.PySide.log import get_logger
9
+
10
+
11
+ class ErrorCenter(QObject):
12
+ error_occurred = Signal(str)
13
+
14
+ def __init__(self):
15
+ super().__init__()
16
+ self._showing_dialog = False
17
+ self.error_occurred.connect(self.show_error_dialog)
18
+ self._logger = get_logger(self.__class__.__name__)
19
+
20
+ def report_exception(
21
+ self,
22
+ exc_type,
23
+ exc_value,
24
+ exc_traceback
25
+ ):
26
+ detail = "".join(
27
+ traceback.format_exception(
28
+ exc_type,
29
+ exc_value,
30
+ exc_traceback,
31
+ )
32
+ )
33
+
34
+ # 无论弹窗是否成功,都先写入日志
35
+ self._logger.critical(detail)
36
+
37
+ # Signal 可以将后台线程中的异常安全地送回 GUI 线程
38
+ self.error_occurred.emit(detail)
39
+
40
+ @Slot(str)
41
+ def show_error_dialog(self, detail):
42
+ if self._showing_dialog:
43
+ return
44
+
45
+ self._showing_dialog = True
46
+
47
+ try:
48
+ box = QMessageBox()
49
+ box.setIcon(QMessageBox.Icon.Critical)
50
+ box.setWindowTitle("喜报")
51
+ box.setText("StartLive开播器 出现未知错误")
52
+ box.setInformativeText(
53
+ "您可以点击下方按钮,了解错误发生的具体原因\n"
54
+ "或前往「安装目录 / logs / StartLive.log」查看日志\n"
55
+ "如果向他人寻求帮助,其你个发送生成的错误报告文件,而不是发送此窗口的截图,录屏"
56
+ "视频,屏幕照片,拍摄的视频,手绘图片或誊抄的画面内容。"
57
+ )
58
+ box.setDetailedText(detail)
59
+ box.setStandardButtons(QMessageBox.StandardButton.Ok)
60
+ box.exec()
61
+ except Exception:
62
+ self._logger.exception("显示错误对话框时再次发生异常")
63
+ finally:
64
+ self._showing_dialog = False
65
+
66
+
67
+ def install_exception_handlers(error_center: ErrorCenter):
68
+ original_hook = sys.excepthook
69
+
70
+ # GUI 主线程以及普通未捕获异常
71
+ def global_exception_hook(exc_type, exc_value, exc_traceback):
72
+ if issubclass(exc_type, KeyboardInterrupt):
73
+ original_hook(exc_type, exc_value, exc_traceback)
74
+ return
75
+
76
+ error_center.report_exception(
77
+ exc_type,
78
+ exc_value,
79
+ exc_traceback,
80
+ )
81
+
82
+ sys.excepthook = global_exception_hook
83
+
84
+ # threading.Thread 中的异常
85
+ def thread_exception_hook(args):
86
+ error_center.report_exception(
87
+ args.exc_type,
88
+ args.exc_value,
89
+ args.exc_traceback
90
+ )
91
+
92
+ threading.excepthook = thread_exception_hook
93
+
94
+ # __del__ 等无法正常抛出的异常
95
+ def unraisable_exception_hook(args):
96
+ error_center.report_exception(
97
+ type(args.exc_value),
98
+ args.exc_value,
99
+ args.exc_traceback
100
+ )
101
+
102
+ sys.unraisablehook = unraisable_exception_hook
@@ -0,0 +1,17 @@
1
+ from PySide6.QtWidgets import QLineEdit
2
+
3
+
4
+ class FocusAwareLineEdit(QLineEdit):
5
+ def __init__(self, parent=None):
6
+ super().__init__(parent)
7
+ self.setEchoMode(
8
+ QLineEdit.EchoMode.Password) # Start with password mode
9
+
10
+ def focusInEvent(self, event):
11
+ super().focusInEvent(event)
12
+ self.setEchoMode(QLineEdit.EchoMode.Normal) # Reveal text when focused
13
+
14
+ def focusOutEvent(self, event):
15
+ super().focusOutEvent(event)
16
+ self.setEchoMode(
17
+ QLineEdit.EchoMode.Password) # Hide text when focus is lost
@@ -0,0 +1,20 @@
1
+ from PySide6.QtWidgets import QLineEdit
2
+
3
+
4
+ class FocusPlaceholderLineEdit(QLineEdit):
5
+ def __init__(self, parent=None):
6
+ super().__init__(parent)
7
+ self._ph = ""
8
+
9
+ def focusInEvent(self, event):
10
+ super().focusInEvent(event)
11
+ self._ph = self.placeholderText()
12
+ self.setPlaceholderText("")
13
+
14
+ def focusOutEvent(self, event):
15
+ super().focusOutEvent(event)
16
+ self.setPlaceholderText(self._ph)
17
+
18
+ def update_placeholder(self, text):
19
+ self._ph = text
20
+ self.setPlaceholderText(text)
@@ -0,0 +1,48 @@
1
+ from PySide6.QtNetwork import QLocalServer, QLocalSocket
2
+ from PySide6.QtWidgets import QMainWindow, QMessageBox
3
+
4
+ from src.core.constant import LOCAL_SERVER_NAME
5
+
6
+
7
+ class SingleInstanceWindow(QMainWindow):
8
+ def __init__(self):
9
+ super().__init__()
10
+ self._server = QLocalServer(self)
11
+ # 清除可能残留的 socket 文件
12
+ QLocalServer.removeServer(LOCAL_SERVER_NAME)
13
+
14
+ if not self._server.listen(LOCAL_SERVER_NAME):
15
+ QMessageBox.critical(self, "应用初始化", "启动本地服务失败!")
16
+ else:
17
+ self._server.newConnection.connect(self._handle_new_connection)
18
+
19
+ def _handle_new_connection(self):
20
+ socket = self._server.nextPendingConnection()
21
+ socket.readyRead.connect(lambda: self._read_and_activate(socket))
22
+
23
+ def _read_and_activate(self, socket):
24
+ data = socket.readAll().data().decode()
25
+ if data == "ACTIVATE":
26
+ self._bring_to_front()
27
+ socket.close()
28
+
29
+ def _bring_to_front(self):
30
+ """唤醒窗口"""
31
+ self.show()
32
+ if self.isMinimized():
33
+ self.showNormal()
34
+ self.raise_()
35
+ self.activateWindow()
36
+
37
+ @staticmethod
38
+ def is_another_instance_running():
39
+ """尝试连接到现有实例"""
40
+ socket = QLocalSocket()
41
+ socket.connectToServer(LOCAL_SERVER_NAME)
42
+ if socket.waitForConnected(50):
43
+ socket.write(b"ACTIVATE")
44
+ socket.flush()
45
+ socket.waitForBytesWritten(50)
46
+ socket.close()
47
+ return True
48
+ return False
@@ -0,0 +1 @@
1
+ from .gui_dispatcher import GUIDispatcher
@@ -0,0 +1,2 @@
1
+ from .announce_update_presenter import AnnounceUpdatePresenter
2
+ from .fetch_announce_presenter import FetchAnnouncePresenter
@@ -0,0 +1,14 @@
1
+ from src.core.workers.base import Presenter
2
+
3
+
4
+ class AnnounceUpdatePresenter(Presenter):
5
+ def __init__(self, view: "StreamConfigPanel") -> None:
6
+ super().__init__()
7
+ self._view = view
8
+
9
+ def prepare_fail_view(self, exception: Exception):
10
+ self._view.save_announce_btn.setEnabled(True)
11
+
12
+ def prepare_success_view(self, *args, **kwargs): ...
13
+
14
+ def prepare_progress_view(self, *args, **kwargs): ...
@@ -0,0 +1,20 @@
1
+ from src.core import app_state
2
+ from src.core.workers.base import Presenter
3
+
4
+
5
+ class FetchAnnouncePresenter(Presenter):
6
+ def __init__(self, view: "StreamConfigPanel") -> None:
7
+ super().__init__()
8
+ self._view = view
9
+
10
+ def prepare_success_view(self):
11
+ self._view.announce_input.setText(app_state.room_info["announcement"])
12
+ self._view.announce_input.textEdited.connect(
13
+ lambda: self._view.save_announce_btn.setEnabled(True))
14
+
15
+ def prepare_progress_view(self, *args, **kwargs):
16
+ self._view.announce_input.setText(app_state.room_info["announcement"])
17
+ self._view.announce_input.textEdited.connect(
18
+ lambda: self._view.save_announce_btn.setEnabled(True))
19
+
20
+ def prepare_fail_view(self, exception: Exception): ...
@@ -0,0 +1,3 @@
1
+ from .area_update_presenter import AreaUpdatePresenter
2
+ from .fetch_area_presenter import FetchAreaPresenter
3
+ from .fetch_recent_area_presenter import FetchRecentAreaPresenter
@@ -0,0 +1,28 @@
1
+ from src.core import app_state
2
+ from src.core.workers.base import Presenter
3
+
4
+
5
+ class AreaUpdatePresenter(Presenter):
6
+ def __init__(self, view: "StreamConfigPanel") -> None:
7
+ super().__init__()
8
+ self._view = view
9
+
10
+ def prepare_success_view(self, area: str):
11
+ app_state.room_info[
12
+ "parent_area"] = app_state.area_reverse[area]
13
+ app_state.room_info[
14
+ "area"] = area
15
+ app_state.room_info["area_code"] = app_state.area_codes[area]
16
+ self._view.parent_combo.setCurrentText(
17
+ app_state.room_info["parent_area"])
18
+ self._view.child_combo.setCurrentText(app_state.room_info["area"])
19
+
20
+ def prepare_fail_view(self, exception: Exception):
21
+ enabled = self._view.enable_child_combo_autosave(False)
22
+ self._view.parent_combo.setCurrentText(
23
+ app_state.room_info["parent_area"])
24
+ self._view.child_combo.setCurrentText(app_state.room_info["area"])
25
+ self._view.enable_child_combo_autosave(enabled)
26
+ self._view.modify_area_btn.setEnabled(True)
27
+
28
+ def prepare_progress_view(self, *args, **kwargs): ...
@@ -0,0 +1,16 @@
1
+ from src.core.workers.base import Presenter
2
+ from ...states import LoginState
3
+
4
+
5
+ class FetchAreaPresenter(Presenter):
6
+ def __init__(self, state: LoginState):
7
+ super().__init__()
8
+ self.state = state
9
+
10
+ def prepare_success_view(self):
11
+ self.state.areaUpdated.emit()
12
+
13
+ def prepare_fail_view(self, exception: Exception):
14
+ self.state.areaUpdated.emit()
15
+
16
+ def prepare_progress_view(self, *args, **kwargs): ...
@@ -0,0 +1,16 @@
1
+ from src.core import app_state
2
+ from src.core.workers.base import Presenter
3
+
4
+
5
+ class FetchRecentAreaPresenter(Presenter):
6
+ def __init__(self, view: "AreaPickerPanel"):
7
+ super().__init__()
8
+ self._view = view
9
+
10
+ def prepare_success_view(self):
11
+ self._view.historyUpdated.emit(app_state.room_info["recent_areas"])
12
+
13
+ def prepare_fail_view(self, exception: Exception):
14
+ self._view.historyUpdated.emit([])
15
+
16
+ def prepare_progress_view(self, *args, **kwargs): ...
@@ -0,0 +1,2 @@
1
+ from .const_update_presenter import ConstantUpdatePresenter
2
+ from .version_checker_presenter import VersionCheckerPresenter
@@ -0,0 +1,19 @@
1
+ from src.PySide.states import LoginState
2
+ from src.core import app_state
3
+ from src.core.workers.base import Presenter
4
+
5
+
6
+ class ConstantUpdatePresenter(Presenter):
7
+ def __init__(self, state: LoginState):
8
+ super().__init__()
9
+ self._state = state
10
+
11
+ def prepare_success_view(self):
12
+ app_state.scan_status["const_updated"] = True
13
+ self._state.constUpdated.emit()
14
+
15
+ def prepare_fail_view(self, exception: Exception):
16
+ app_state.scan_status["const_updated"] = True
17
+ self._state.constUpdated.emit()
18
+
19
+ def prepare_progress_view(self, progress: int): ...
@@ -0,0 +1,16 @@
1
+ from src.PySide.states import LoginState
2
+ from src.core.workers.base import Presenter
3
+
4
+
5
+ class VersionCheckerPresenter(Presenter):
6
+ def __init__(self, state: LoginState):
7
+ super().__init__()
8
+ self._state = state
9
+
10
+ def prepare_success_view(self, version: str | None = None):
11
+ if version is not None:
12
+ self._state.versionChecked.emit(version)
13
+
14
+ def prepare_fail_view(self, exception: Exception): ...
15
+
16
+ def prepare_progress_view(self, *args, **kwargs): ...
@@ -0,0 +1,3 @@
1
+ from .cover_state_presenter import CoverStateUpdatePresenter
2
+ from .cover_upload_presenter import CoverUploadPresenter
3
+ from .fetch_cover_presenter import FetchCoverPresenter