Skip to content

Commit ae4ce2b

Browse files
committed
feat: integrate web support
1 parent e5f3122 commit ae4ce2b

20 files changed

+174
-57
lines changed

app/core/record_manager.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import threading
23
from datetime import datetime, timedelta
34

45
from ..messages.message_pusher import MessagePusher
@@ -10,11 +11,15 @@
1011
from .stream_manager import LiveStreamRecorder
1112

1213

14+
class GlobalRecordingState:
15+
recordings = []
16+
lock = threading.Lock()
17+
18+
1319
class RecordingManager:
1420
def __init__(self, app):
1521
self.app = app
1622
self.settings = app.settings
17-
self.recordings = []
1823
self.periodic_task_started = False
1924
self.loop_time_seconds = None
2025
self.app.language_manager.add_observer(self)
@@ -23,6 +28,14 @@ def __init__(self, app):
2328
self.load()
2429
self.initialize_dynamic_state()
2530

31+
@property
32+
def recordings(self):
33+
return GlobalRecordingState.recordings
34+
35+
@recordings.setter
36+
def recordings(self, value):
37+
raise AttributeError("Please use add_recording/update_recording methods to modify data")
38+
2639
def load(self):
2740
language = self.app.language_manager.language
2841
for key in ("recording_manager", "video_quality"):
@@ -31,7 +44,8 @@ def load(self):
3144
def load_recordings(self):
3245
"""Load recordings from a JSON file into objects."""
3346
recordings_data = self.app.config_manager.load_recordings_config()
34-
self.recordings = [Recording.from_dict(rec) for rec in recordings_data]
47+
if not GlobalRecordingState.recordings:
48+
GlobalRecordingState.recordings = [Recording.from_dict(rec) for rec in recordings_data]
3549
logger.info(f"Live Recordings: Loaded {len(self.recordings)} items")
3650

3751
def initialize_dynamic_state(self):
@@ -42,11 +56,31 @@ def initialize_dynamic_state(self):
4256
recording.loop_time_seconds = self.loop_time_seconds
4357
recording.update_title(self._[recording.quality])
4458

45-
async def update_recording(self, recording: Recording, updated_info: dict):
59+
async def add_recording(self, recording):
60+
with GlobalRecordingState.lock:
61+
GlobalRecordingState.recordings.append(recording)
62+
await self.persist_recordings()
63+
64+
async def remove_recording(self, recording: Recording):
65+
with GlobalRecordingState.lock:
66+
GlobalRecordingState.recordings.remove(recording)
67+
await self.persist_recordings()
68+
69+
async def clear_all_recordings(self):
70+
with GlobalRecordingState.lock:
71+
GlobalRecordingState.recordings.clear()
72+
await self.persist_recordings()
73+
74+
async def persist_recordings(self):
75+
"""Persist recordings to a JSON file."""
76+
data_to_save = [rec.to_dict() for rec in self.recordings]
77+
await self.app.config_manager.save_recordings_config(data_to_save)
78+
79+
async def update_recording_card(self, recording: Recording, updated_info: dict):
4680
"""Update an existing recording object and persist changes to a JSON file."""
4781
if recording:
4882
recording.update(updated_info)
49-
self.app.page.run_task(self.save_to_json)
83+
self.app.page.run_task(self.persist_recordings)
5084

5185
@staticmethod
5286
async def _update_recording(
@@ -74,9 +108,10 @@ async def start_monitor_recording(self, recording: Recording, auto_save: bool =
74108
selected=False,
75109
)
76110
self.app.page.run_task(self.check_if_live, recording)
77-
self.app.page.run_task(self.app.record_card_manager.update_cards, recording)
111+
self.app.page.run_task(self.app.record_card_manager.update_card, recording)
112+
self.app.page.pubsub.send_others_on_topic("update", recording)
78113
if auto_save:
79-
self.app.page.run_task(self.save_to_json)
114+
self.app.page.run_task(self.persist_recordings)
80115

81116
async def stop_monitor_recording(self, recording: Recording, auto_save: bool = True):
82117
"""
@@ -91,9 +126,10 @@ async def stop_monitor_recording(self, recording: Recording, auto_save: bool = T
91126
selected=False,
92127
)
93128
self.stop_recording(recording)
94-
self.app.page.run_task(self.app.record_card_manager.update_cards, recording)
129+
self.app.page.run_task(self.app.record_card_manager.update_card, recording)
130+
self.app.page.pubsub.send_others_on_topic("update", recording)
95131
if auto_save:
96-
self.app.page.run_task(self.save_to_json)
132+
self.app.page.run_task(self.persist_recordings)
97133

98134
async def start_monitor_recordings(self):
99135
"""
@@ -105,7 +141,7 @@ async def start_monitor_recordings(self):
105141
for recording in pre_start_monitor_recordings:
106142
if cards_obj[recording.rec_id]["card"].visible:
107143
self.app.page.run_task(self.start_monitor_recording, recording, auto_save=False)
108-
self.app.page.run_task(self.save_to_json)
144+
self.app.page.run_task(self.persist_recordings)
109145
logger.info(f"Batch Start Monitor Recordings: {[i.rec_id for i in pre_start_monitor_recordings]}")
110146

111147
async def stop_monitor_recordings(self, selected_recordings: list[Recording | None] | None = None):
@@ -119,19 +155,18 @@ async def stop_monitor_recordings(self, selected_recordings: list[Recording | No
119155
for recording in pre_stop_monitor_recordings:
120156
if cards_obj[recording.rec_id]["card"].visible:
121157
self.app.page.run_task(self.stop_monitor_recording, recording, auto_save=False)
122-
self.app.page.run_task(self.save_to_json)
158+
self.app.page.run_task(self.persist_recordings)
123159
logger.info(f"Batch Stop Monitor Recordings: {[i.rec_id for i in pre_stop_monitor_recordings]}")
124160

125161
async def get_selected_recordings(self):
126162
return [recording for recording in self.recordings if recording.selected]
127163

128-
def remove_recordings(self, recordings: list[Recording]):
164+
async def remove_recordings(self, recordings: list[Recording]):
129165
"""Remove a recording from the list and update the JSON file."""
130166
for recording in recordings:
131167
if recording in self.recordings:
132-
self.recordings.remove(recording)
168+
await self.remove_recording(recording)
133169
logger.info(f"Delete Items: {recording.rec_id}-{recording.streamer_name}")
134-
self.app.page.run_task(self.save_to_json)
135170

136171
def find_recording_by_id(self, rec_id: str):
137172
"""Find a recording by its ID (hash of dict representation)."""
@@ -140,11 +175,6 @@ def find_recording_by_id(self, rec_id: str):
140175
return rec
141176
return None
142177

143-
async def save_to_json(self):
144-
"""Persist recordings to a JSON file."""
145-
recordings_data = [rec.to_dict() for rec in self.recordings]
146-
await self.app.config_manager.save_recordings_config(recordings_data)
147-
148178
async def check_all_live_status(self):
149179
"""Check the live status of all recordings and update their display titles."""
150180
for recording in self.recordings:
@@ -264,7 +294,8 @@ async def check_if_live(self, recording: Recording):
264294
self.start_update(recording)
265295
self.app.page.run_task(recorder.start_recording, stream_info)
266296

267-
self.app.page.run_task(self.app.record_card_manager.update_cards, recording)
297+
self.app.page.run_task(self.app.record_card_manager.update_card, recording)
298+
self.app.page.pubsub.send_others_on_topic("update", recording)
268299

269300
else:
270301
recording.status_info = RecordingStatus.MONITORING
@@ -278,8 +309,9 @@ async def check_if_live(self, recording: Recording):
278309
"display_title": title,
279310
}
280311
)
281-
self.app.page.run_task(self.app.record_card_manager.update_cards, recording)
282-
self.app.page.run_task(self.save_to_json)
312+
self.app.page.run_task(self.app.record_card_manager.update_card, recording)
313+
self.app.page.pubsub.send_others_on_topic("update", recording)
314+
self.app.page.run_task(self.persist_recordings)
283315

284316
@staticmethod
285317
def start_update(recording: Recording):
@@ -323,8 +355,9 @@ def get_duration(self, recording: Recording):
323355
return str(total_duration).split(".")[0]
324356

325357
async def delete_recording_cards(self, recordings: list[Recording]):
326-
self.remove_recordings(recordings)
327358
self.app.page.run_task(self.app.record_card_manager.remove_recording_card, recordings)
359+
self.app.page.pubsub.send_others_on_topic('delete', recordings)
360+
await self.remove_recordings(recordings)
328361

329362
async def check_free_space(self, output_dir: str | None = None):
330363
disk_space_limit = float(self.settings.user_config.get("recording_space_threshold"))
@@ -342,4 +375,4 @@ async def check_free_space(self, output_dir: str | None = None):
342375
)
343376

344377
else:
345-
self.app.recording_enabled = True
378+
self.app.recording_enabled = True

app/core/stream_manager.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def _get_output_dir(self, stream_info: StreamData) -> str:
9393
output_dir = os.path.join(output_dir, f"{now[:10]}_{live_title}")
9494
os.makedirs(output_dir, exist_ok=True)
9595
self.recording.recording_dir = output_dir
96-
self.app.page.run_task(self.app.record_manager.save_to_json)
96+
self.app.page.run_task(self.app.record_manager.persist_recordings)
9797
return output_dir
9898

9999
def _get_save_path(self, filename: str) -> str:
@@ -225,18 +225,19 @@ async def start_ffmpeg(
225225

226226
await asyncio.sleep(1)
227227

228+
return_code = process.returncode
229+
safe_return_code = [0, 255]
228230
stdout, stderr = await process.communicate()
229-
if stderr:
231+
if return_code not in safe_return_code and stderr:
230232
logger.error(f"FFmpeg Stderr Output: {str(stderr.decode()).splitlines()[0]}")
231233
self.recording.status_info = RecordingStatus.RECORDING_ERROR
232234
self.app.record_manager.stop_recording(self.recording)
233-
await self.app.record_card_manager.update_cards(self.recording)
235+
await self.app.record_card_manager.update_card(self.recording)
236+
self.app.page.pubsub.send_others_on_topic("update", self.recording)
234237
await self.app.snack_bar.show_snack_bar(
235238
record_name + " " + self._["record_stream_error"], duration=2000
236239
)
237240

238-
return_code = process.returncode
239-
safe_return_code = [0, 255]
240241
if return_code in safe_return_code:
241242
if self.recording.monitor_status:
242243
self.recording.status_info = RecordingStatus.MONITORING
@@ -253,7 +254,8 @@ async def start_ffmpeg(
253254
logger.success(f"Live recording completed: {record_name}")
254255

255256
self.recording.update({"display_title": display_title})
256-
self.app.page.run_task(self.app.record_card_manager.update_cards, self.recording)
257+
await self.app.record_card_manager.update_card(self.recording)
258+
self.app.page.pubsub.send_others_on_topic("update", self.recording)
257259
if self.app.recording_enabled and process in self.app.process_manager.ffmpeg_processes:
258260
self.app.page.run_task(self.app.record_manager.check_if_live, self.recording)
259261
else:
@@ -400,6 +402,6 @@ def get_headers_params(live_url, platform_key):
400402
"qiandurebo": "referer:https://qiandurebo.com",
401403
"17live": "referer:https://17.live/en/live/6302408",
402404
"lang": "referer:https://www.lang.live",
403-
"shopee": f"origin:{live_domain}",
405+
"shopee": "origin:" + live_domain,
404406
}
405-
return record_headers.get(platform_key)
407+
return record_headers.get(platform_key)

app/ui/components/recording_card.py

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ def __init__(self, app):
2020
self.app.language_manager.add_observer(self)
2121
self._ = {}
2222
self.load()
23+
self.pubsub_subscribe()
2324

2425
def load(self):
2526
language = self.app.language_manager.language
2627
for key in ("recording_card", "recording_manager", "base", "home_page", "video_quality"):
2728
self._.update(language.get(key, {}))
2829

30+
def pubsub_subscribe(self):
31+
self.app.page.pubsub.subscribe_topic("update", self.subscribe_update_card)
32+
self.app.page.pubsub.subscribe_topic("delete", self.subscribe_remove_cards)
33+
2934
async def create_card(self, recording: Recording):
3035
"""Create a card for a given recording."""
3136
rec_id = recording.rec_id
@@ -124,7 +129,7 @@ def _create_card_components(self, recording: Recording):
124129
"monitor_button": monitor_button,
125130
}
126131

127-
async def update_cards(self, recording):
132+
async def update_card(self, recording):
128133
"""Update only the recordings cards in the scrollable content area."""
129134
if recording.rec_id in self.cards_obj:
130135
recording_card = self.cards_obj[recording.rec_id]
@@ -161,8 +166,10 @@ async def update_monitor_state(self, recording: Recording):
161166
)
162167
self.app.page.run_task(self.app.record_manager.check_if_live, recording)
163168
self.app.page.run_task(self.app.snack_bar.show_snack_bar, self._["start_monitor_tip"], ft.Colors.GREEN)
164-
await self.update_cards(recording)
165-
self.app.page.run_task(self.app.record_manager.save_to_json)
169+
170+
await self.update_card(recording)
171+
self.app.page.pubsub.send_others_on_topic("update", recording)
172+
self.app.page.run_task(self.app.record_manager.persist_recordings)
166173

167174
async def show_recording_info_dialog(self, recording: Recording):
168175
"""Display a dialog with detailed information about the recording."""
@@ -175,10 +182,11 @@ async def edit_recording_callback(self, recording_list: list[dict]):
175182
recording = recording_list[0]
176183
rec_id = recording["rec_id"]
177184
recording_obj = self.app.record_manager.find_recording_by_id(rec_id)
178-
await self.app.record_manager.update_recording(recording_obj, updated_info=recording)
185+
await self.app.record_manager.update_recording_card(recording_obj, updated_info=recording)
179186
if not recording["monitor_status"]:
180187
recording_obj.display_title = f"[{self._['monitor_stopped']}] " + recording_obj.title
181-
await self.update_cards(recording_obj)
188+
await self.update_card(recording_obj)
189+
self.app.page.pubsub.send_others_on_topic("update", recording)
182190

183191
async def on_toggle_recording(self, recording: Recording):
184192
"""Toggle the recording state for a specific recording."""
@@ -197,7 +205,8 @@ async def on_toggle_recording(self, recording: Recording):
197205
else:
198206
await self.app.snack_bar.show_snack_bar(self._["please_start_monitor_tip"])
199207

200-
await self.update_cards(recording)
208+
await self.update_card(recording)
209+
self.app.page.pubsub.send_others_on_topic("update", recording)
201210

202211
async def on_delete_recording(self, recording: Recording):
203212
"""Delete a recording from the list and update UI."""
@@ -212,11 +221,27 @@ async def on_delete_recording(self, recording: Recording):
212221

213222
async def remove_recording_card(self, recordings: list[Recording]):
214223
home_page = self.app.current_page
215-
for recording in recordings:
216-
if recording.rec_id in self.cards_obj:
217-
card = self.cards_obj[recording.rec_id]["card"]
218-
home_page.recording_card_area.controls.remove(card)
219-
del self.cards_obj[recording.rec_id]
224+
225+
existing_ids = {rec.rec_id for rec in self.app.record_manager.recordings}
226+
remove_ids = {rec.rec_id for rec in recordings}
227+
keep_ids = existing_ids - remove_ids
228+
229+
cards_to_remove = [
230+
card_data["card"]
231+
for rec_id, card_data in self.cards_obj.items()
232+
if rec_id not in keep_ids
233+
]
234+
235+
home_page.recording_card_area.controls = [
236+
control
237+
for control in home_page.recording_card_area.controls
238+
if control not in cards_to_remove
239+
]
240+
241+
self.cards_obj = {
242+
k: v for k, v in self.cards_obj.items()
243+
if k in keep_ids
244+
}
220245
home_page.recording_card_area.update()
221246

222247
@staticmethod
@@ -318,3 +343,9 @@ async def monitor_button_on_click(self, _, recording: Recording):
318343

319344
async def recording_card_on_click(self, _, recording: Recording):
320345
await self.on_card_click(recording)
346+
347+
async def subscribe_update_card(self, _, recording: Recording):
348+
await self.update_card(recording)
349+
350+
async def subscribe_remove_cards(self, _, recordings: list[Recording]):
351+
await self.remove_recording_card(recordings)

0 commit comments

Comments
 (0)