@danhachuel/thunderbolt 0.7.59 → 0.7.61
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.
- package/app/main.py +21 -20
- package/integrations/platforms.py +56 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -9524,26 +9524,27 @@ def render_notifications():
|
|
|
9524
9524
|
st.rerun()
|
|
9525
9525
|
|
|
9526
9526
|
st.divider()
|
|
9527
|
-
st.
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
event["
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9527
|
+
with st.expander("Operações notificadas", expanded=False):
|
|
9528
|
+
st.subheader("Operações notificadas")
|
|
9529
|
+
st.caption("Ligue ou desligue cada tipo de notificação. As preferências ficam guardadas no storage local e aplicam-se às próximas conclusões.")
|
|
9530
|
+
grouped: dict[str, list[dict[str, str]]] = {}
|
|
9531
|
+
for event in catalog:
|
|
9532
|
+
grouped.setdefault(event["category"], []).append(event)
|
|
9533
|
+
with st.form("notification_preferences_form"):
|
|
9534
|
+
pending_preferences: dict[str, bool] = {}
|
|
9535
|
+
for category, events in grouped.items():
|
|
9536
|
+
st.markdown(f"**{category}**")
|
|
9537
|
+
for event in events:
|
|
9538
|
+
pending_preferences[event["code"]] = st.checkbox(
|
|
9539
|
+
event["label"],
|
|
9540
|
+
value=bool(preferences.get(event["code"], True)),
|
|
9541
|
+
help=event["description"],
|
|
9542
|
+
key=f"notification_preference_{event['code']}",
|
|
9543
|
+
)
|
|
9544
|
+
if st.form_submit_button("Guardar preferências", type="primary", width="stretch"):
|
|
9545
|
+
save_notification_preferences(pending_preferences)
|
|
9546
|
+
st.success("Preferências de notificação guardadas.")
|
|
9547
|
+
st.rerun()
|
|
9547
9548
|
|
|
9548
9549
|
st.divider()
|
|
9549
9550
|
st.subheader("Histórico de notificações")
|
|
@@ -248,6 +248,45 @@ def _public_feed_data(channel_id: str, headers: dict[str, str]) -> dict[str, Any
|
|
|
248
248
|
return {}
|
|
249
249
|
|
|
250
250
|
|
|
251
|
+
def _public_page_videos(document: str, channel_id: str, limit: int) -> list[dict[str, Any]]:
|
|
252
|
+
"""Extract public video cards from ytInitialData as an RSS fallback."""
|
|
253
|
+
initial_data = _extract_json_assignment(document, "ytInitialData") or {}
|
|
254
|
+
videos: list[dict[str, Any]] = []
|
|
255
|
+
|
|
256
|
+
def visit(value: Any) -> None:
|
|
257
|
+
if len(videos) >= limit:
|
|
258
|
+
return
|
|
259
|
+
if isinstance(value, dict):
|
|
260
|
+
renderer = value.get("videoRenderer")
|
|
261
|
+
if isinstance(renderer, dict):
|
|
262
|
+
video_id = str(renderer.get("videoId") or "").strip()
|
|
263
|
+
if video_id:
|
|
264
|
+
title = _text_from_node(renderer.get("title")) or "Vídeo sem título"
|
|
265
|
+
thumbnails = ((renderer.get("thumbnail") or {}).get("thumbnails") or [])
|
|
266
|
+
thumbnail = str(thumbnails[-1].get("url", "")) if thumbnails and isinstance(thumbnails[-1], dict) else ""
|
|
267
|
+
videos.append({
|
|
268
|
+
"id": f"youtube_{video_id}",
|
|
269
|
+
"youtube_video_id": video_id,
|
|
270
|
+
"channel_id": channel_id,
|
|
271
|
+
"title": title,
|
|
272
|
+
"published_at": _text_from_node(renderer.get("publishedTimeText")),
|
|
273
|
+
"updated_at": "",
|
|
274
|
+
"url": f"https://www.youtube.com/watch?v={video_id}",
|
|
275
|
+
"thumbnail_url": thumbnail,
|
|
276
|
+
"source": "youtube_public_page",
|
|
277
|
+
"status": "publicado",
|
|
278
|
+
})
|
|
279
|
+
return
|
|
280
|
+
for child in value.values():
|
|
281
|
+
visit(child)
|
|
282
|
+
elif isinstance(value, list):
|
|
283
|
+
for child in value:
|
|
284
|
+
visit(child)
|
|
285
|
+
|
|
286
|
+
visit(initial_data)
|
|
287
|
+
return videos[:limit]
|
|
288
|
+
|
|
289
|
+
|
|
251
290
|
def fetch_channel_videos_public(channel_ref: str | dict[str, Any], limit: int = 10) -> "IntegrationResult":
|
|
252
291
|
"""Fetch the latest public channel videos from YouTube's Atom feed without an API key."""
|
|
253
292
|
if isinstance(channel_ref, dict):
|
|
@@ -281,6 +320,23 @@ def fetch_channel_videos_public(channel_ref: str | dict[str, Any], limit: int =
|
|
|
281
320
|
response.raise_for_status()
|
|
282
321
|
root = ET.fromstring(response.text)
|
|
283
322
|
except (requests.RequestException, ET.ParseError, ValueError) as exc:
|
|
323
|
+
for page_url in _public_page_candidates(f"https://www.youtube.com/channel/{channel_id}"):
|
|
324
|
+
try:
|
|
325
|
+
page_response = requests.get(
|
|
326
|
+
page_url,
|
|
327
|
+
headers={**headers, "Accept": "text/html,application/xhtml+xml"},
|
|
328
|
+
timeout=12,
|
|
329
|
+
)
|
|
330
|
+
page_response.raise_for_status()
|
|
331
|
+
except requests.RequestException:
|
|
332
|
+
continue
|
|
333
|
+
fallback_videos = _public_page_videos(page_response.text or "", channel_id, max(1, min(10, int(limit or 10))))
|
|
334
|
+
if fallback_videos:
|
|
335
|
+
return IntegrationResult(
|
|
336
|
+
True,
|
|
337
|
+
f"{len(fallback_videos)} vídeo(s) público(s) carregado(s) pela página do canal sem API Key.",
|
|
338
|
+
{"channel_id": channel_id, "videos": fallback_videos},
|
|
339
|
+
)
|
|
284
340
|
return IntegrationResult(False, f"Não foi possível carregar os vídeos públicos do canal: {exc}", {"channel_id": channel_id, "videos": []})
|
|
285
341
|
namespace = {
|
|
286
342
|
"yt": "http://www.youtube.com/xml/schemas/2015",
|
package/package.json
CHANGED