@danhachuel/thunderbolt 0.5.55 → 0.5.57
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 +15 -3
- package/package.json +1 -1
- package/scripts/streamlit_bootstrap.py +27 -2
package/app/main.py
CHANGED
|
@@ -2340,6 +2340,16 @@ def is_youtube_channel_record(channel: Any) -> bool:
|
|
|
2340
2340
|
return classify_channel_platform(channel) == "youtube"
|
|
2341
2341
|
|
|
2342
2342
|
|
|
2343
|
+
def _format_channel_count(value: Any) -> str:
|
|
2344
|
+
"""Mostrar contadores inteiros com ponto a cada três algarismos."""
|
|
2345
|
+
if value is None or str(value).strip() == "":
|
|
2346
|
+
return "—"
|
|
2347
|
+
try:
|
|
2348
|
+
return f"{int(value):,}".replace(",", ".")
|
|
2349
|
+
except (TypeError, ValueError):
|
|
2350
|
+
return str(value)
|
|
2351
|
+
|
|
2352
|
+
|
|
2343
2353
|
def render_channels():
|
|
2344
2354
|
st.title("Canais Youtube")
|
|
2345
2355
|
st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
|
|
@@ -2810,7 +2820,7 @@ def render_channels():
|
|
|
2810
2820
|
channel_id = str(channel["id"])
|
|
2811
2821
|
edit_key = f"edit_channel_{channel_id}"
|
|
2812
2822
|
with st.container(border=True):
|
|
2813
|
-
header_cols = st.columns([0.7, 3.
|
|
2823
|
+
header_cols = st.columns([0.7, 3.1, 1.25, 1.15, 1.55, 1.5])
|
|
2814
2824
|
with header_cols[0]:
|
|
2815
2825
|
if channel.get("thumbnail_url"):
|
|
2816
2826
|
st.image(channel["thumbnail_url"], width=64)
|
|
@@ -2821,10 +2831,12 @@ def render_channels():
|
|
|
2821
2831
|
st.caption(channel_niche_label(channel))
|
|
2822
2832
|
st.caption(f"{channel.get('handle') or channel.get('url') or 'sem URL'} · {channel.get('metrics_source', 'manual')}")
|
|
2823
2833
|
with header_cols[2]:
|
|
2824
|
-
st.metric("Inscritos",
|
|
2834
|
+
st.metric("Inscritos", _format_channel_count(channel.get("subscriber_count")))
|
|
2825
2835
|
with header_cols[3]:
|
|
2826
|
-
st.metric("Vídeos",
|
|
2836
|
+
st.metric("Vídeos", _format_channel_count(channel.get("video_count")))
|
|
2827
2837
|
with header_cols[4]:
|
|
2838
|
+
st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
|
|
2839
|
+
with header_cols[5]:
|
|
2828
2840
|
active = st.toggle("Activo", value=channel.get("active", True), key=f"active_{channel_id}")
|
|
2829
2841
|
if active != channel.get("active"):
|
|
2830
2842
|
update_channel(channel_id, {"active": active})
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import io
|
|
5
|
+
import logging
|
|
4
6
|
import os
|
|
5
7
|
import signal
|
|
6
8
|
import sys
|
|
9
|
+
import time
|
|
7
10
|
|
|
8
11
|
# Este ficheiro é o primeiro módulo Python executado pelo launcher. As
|
|
9
12
|
# variáveis e os streams precisam de ser corrigidos antes de qualquer import
|
|
@@ -14,6 +17,12 @@ os.environ["PYTHONLEGACYWINDOWSSTDIO"] = "1"
|
|
|
14
17
|
os.environ["CLICK_NO_WIN_CONSOLE"] = "1"
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
# O shutdown do Streamlit pode emitir tracebacks assíncronos depois de SIGINT.
|
|
21
|
+
# Estes níveis só afectam a saída de encerramento, não os logs da aplicação.
|
|
22
|
+
logging.getLogger("uvicorn").setLevel(logging.CRITICAL)
|
|
23
|
+
logging.getLogger("streamlit").setLevel(logging.CRITICAL)
|
|
24
|
+
|
|
25
|
+
|
|
17
26
|
def _utf8_stream(stream: object) -> object:
|
|
18
27
|
if os.name != "nt":
|
|
19
28
|
return stream
|
|
@@ -30,10 +39,26 @@ sys.stdout = _utf8_stream(sys.stdout)
|
|
|
30
39
|
sys.stderr = _utf8_stream(sys.stderr)
|
|
31
40
|
|
|
32
41
|
|
|
42
|
+
_shutdown_started = False
|
|
43
|
+
|
|
44
|
+
|
|
33
45
|
def _custom_sigint_handler(signum: int, frame: object) -> None:
|
|
34
|
-
"""
|
|
46
|
+
"""Parar o loop, dar uma pequena janela de drenagem e sair sem traceback."""
|
|
47
|
+
global _shutdown_started
|
|
48
|
+
if _shutdown_started:
|
|
49
|
+
return
|
|
50
|
+
_shutdown_started = True
|
|
35
51
|
print("\nEncerrando servidor...", file=sys.stderr, flush=True)
|
|
36
|
-
|
|
52
|
+
try:
|
|
53
|
+
loop = asyncio.get_running_loop()
|
|
54
|
+
except RuntimeError:
|
|
55
|
+
loop = None
|
|
56
|
+
if loop is not None and not loop.is_closed():
|
|
57
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
58
|
+
# O processo pai (cli.mjs) encerra proxy e workers quando o filho termina.
|
|
59
|
+
# os._exit evita que callbacks tardios do Streamlit imprimam tracebacks.
|
|
60
|
+
time.sleep(0.5)
|
|
61
|
+
os._exit(0)
|
|
37
62
|
|
|
38
63
|
|
|
39
64
|
_original_signal = signal.signal
|