@danhachuel/thunderbolt 0.7.52 → 0.7.54
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 +41 -6
- package/hermes_ui/storage.py +32 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +11 -8
package/app/main.py
CHANGED
|
@@ -3326,16 +3326,33 @@ def render_channels():
|
|
|
3326
3326
|
channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
|
|
3327
3327
|
st.divider()
|
|
3328
3328
|
st.subheader(f"Canais Youtube cadastrados ({len(channels)})")
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3329
|
+
filter_cols = st.columns([1, 1], gap="small")
|
|
3330
|
+
with filter_cols[0]:
|
|
3331
|
+
youtube_channel_search = st.text_input(
|
|
3332
|
+
"Pesquisar nomes dos canais YouTube",
|
|
3333
|
+
key="youtube_registered_channels_search",
|
|
3334
|
+
placeholder="Digite o nome do canal",
|
|
3335
|
+
)
|
|
3336
|
+
with filter_cols[1]:
|
|
3337
|
+
youtube_country_options = [""] + list(COUNTRY_OPTIONS)
|
|
3338
|
+
youtube_country_filter = st.selectbox(
|
|
3339
|
+
"Filtrar por País",
|
|
3340
|
+
youtube_country_options,
|
|
3341
|
+
format_func=lambda value: value or "Todos os países",
|
|
3342
|
+
key="youtube_registered_country_filter",
|
|
3343
|
+
)
|
|
3344
|
+
if st.button("Filtrar", type="primary", width="content", key="youtube_registered_country_filter_apply"):
|
|
3345
|
+
st.session_state["youtube_registered_country_filter_applied"] = youtube_country_filter
|
|
3346
|
+
applied_country = str(st.session_state.get("youtube_registered_country_filter_applied") or "").strip()
|
|
3334
3347
|
visible_registered_channels = [
|
|
3335
3348
|
channel for channel in channels
|
|
3336
3349
|
if not youtube_channel_search.strip()
|
|
3337
3350
|
or youtube_channel_search.strip().casefold() in str(channel.get("name") or "").casefold()
|
|
3338
3351
|
]
|
|
3352
|
+
visible_registered_channels = [
|
|
3353
|
+
channel for channel in visible_registered_channels
|
|
3354
|
+
if not applied_country or str(channel.get("country") or "").strip() == applied_country
|
|
3355
|
+
]
|
|
3339
3356
|
if not channels:
|
|
3340
3357
|
st.info("Nenhum canal YouTube cadastrado.")
|
|
3341
3358
|
return
|
|
@@ -6480,7 +6497,24 @@ def render_automation():
|
|
|
6480
6497
|
if not channels:
|
|
6481
6498
|
st.info("Nenhum canal cadastrado para configurar.")
|
|
6482
6499
|
with st.expander("Canais cadastrados", expanded=False):
|
|
6483
|
-
|
|
6500
|
+
automation_country_options = [""] + list(COUNTRY_OPTIONS)
|
|
6501
|
+
automation_filter_cols = st.columns([1, 1], gap="small")
|
|
6502
|
+
with automation_filter_cols[0]:
|
|
6503
|
+
automation_country_filter = st.selectbox(
|
|
6504
|
+
"Filtrar por País",
|
|
6505
|
+
automation_country_options,
|
|
6506
|
+
format_func=lambda value: value or "Todos os países",
|
|
6507
|
+
key="youtube_automation_country_filter",
|
|
6508
|
+
)
|
|
6509
|
+
with automation_filter_cols[1]:
|
|
6510
|
+
if st.button("Filtrar", type="primary", width="content", key="youtube_automation_country_filter_apply"):
|
|
6511
|
+
st.session_state["youtube_automation_country_filter_applied"] = automation_country_filter
|
|
6512
|
+
applied_automation_country = str(st.session_state.get("youtube_automation_country_filter_applied") or "").strip()
|
|
6513
|
+
filtered_automation_channels = [
|
|
6514
|
+
channel for channel in channels
|
|
6515
|
+
if not applied_automation_country or str(channel.get("country") or "").strip() == applied_automation_country
|
|
6516
|
+
]
|
|
6517
|
+
for channel in filtered_automation_channels:
|
|
6484
6518
|
channel_id = channel["id"]
|
|
6485
6519
|
with st.container(border=True):
|
|
6486
6520
|
blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
|
|
@@ -6497,6 +6531,7 @@ def render_automation():
|
|
|
6497
6531
|
with header_cols[1]:
|
|
6498
6532
|
st.write(f"**{channel.get('name', 'Sem nome')}**")
|
|
6499
6533
|
st.caption(channel.get("handle") or channel.get("url") or "sem URL")
|
|
6534
|
+
st.caption(f"País: {channel.get('country') or 'Não definido'}")
|
|
6500
6535
|
channel_url = str(channel.get("url") or "").strip()
|
|
6501
6536
|
if not channel_url:
|
|
6502
6537
|
channel_id_or_handle = str(channel.get("youtube_channel_id") or channel.get("youtube_id") or channel.get("handle") or "").strip()
|
package/hermes_ui/storage.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
|
+
from threading import RLock
|
|
8
9
|
from contextlib import contextmanager
|
|
9
10
|
from copy import deepcopy
|
|
10
11
|
from datetime import datetime, timezone
|
|
@@ -24,6 +25,9 @@ SEED_BLUEPRINTS = ROOT / "seed" / "blueprints"
|
|
|
24
25
|
SEED_THUMBNAIL_BLUEPRINTS = SEED_BLUEPRINTS / "thumbnails"
|
|
25
26
|
SEED_TIKTOK_PROMPT_MASTERS = ROOT / "seed" / "prompt_masters"
|
|
26
27
|
|
|
28
|
+
_READ_CACHE_LOCK = RLock()
|
|
29
|
+
_READ_CACHE: dict[str, tuple[tuple[int, int], Any]] = {}
|
|
30
|
+
|
|
27
31
|
DEFAULTS: dict[str, Any] = {
|
|
28
32
|
"channels.json": [],
|
|
29
33
|
"channel_videos.json": [],
|
|
@@ -720,6 +724,19 @@ def _load_json_unlocked(path: Path) -> Any:
|
|
|
720
724
|
return json.load(handle)
|
|
721
725
|
|
|
722
726
|
|
|
727
|
+
def _read_fingerprint(path: Path) -> tuple[int, int] | None:
|
|
728
|
+
try:
|
|
729
|
+
stat = path.stat()
|
|
730
|
+
except OSError:
|
|
731
|
+
return None
|
|
732
|
+
return stat.st_mtime_ns, stat.st_size
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _invalidate_read_cache(path: Path) -> None:
|
|
736
|
+
with _READ_CACHE_LOCK:
|
|
737
|
+
_READ_CACHE.pop(str(path), None)
|
|
738
|
+
|
|
739
|
+
|
|
723
740
|
def _recover_json_unlocked(name: str, path: Path, default: Any | None) -> Any:
|
|
724
741
|
backup = _corrupt_backup(path)
|
|
725
742
|
if name in _PROTECTED_STATE_FILES:
|
|
@@ -743,6 +760,13 @@ def _recover_json_unlocked(name: str, path: Path, default: Any | None) -> Any:
|
|
|
743
760
|
def read_json(name: str, default: Any | None = None) -> Any:
|
|
744
761
|
ensure_storage()
|
|
745
762
|
path = STATE / name
|
|
763
|
+
fingerprint = _read_fingerprint(path)
|
|
764
|
+
cache_key = str(path)
|
|
765
|
+
if fingerprint is not None:
|
|
766
|
+
with _READ_CACHE_LOCK:
|
|
767
|
+
cached = _READ_CACHE.get(cache_key)
|
|
768
|
+
if cached is not None and cached[0] == fingerprint:
|
|
769
|
+
return deepcopy(cached[1])
|
|
746
770
|
with _state_lock(path, read_only=True):
|
|
747
771
|
try:
|
|
748
772
|
data = _load_json_unlocked(path)
|
|
@@ -753,7 +777,12 @@ def read_json(name: str, default: Any | None = None) -> Any:
|
|
|
753
777
|
migrated, changed = _migrate_settings(data)
|
|
754
778
|
if changed:
|
|
755
779
|
_atomic_write_unlocked(path, migrated)
|
|
756
|
-
|
|
780
|
+
result = migrated
|
|
781
|
+
current_fingerprint = _read_fingerprint(path)
|
|
782
|
+
if current_fingerprint is not None:
|
|
783
|
+
with _READ_CACHE_LOCK:
|
|
784
|
+
_READ_CACHE[cache_key] = (current_fingerprint, deepcopy(result))
|
|
785
|
+
return deepcopy(result)
|
|
757
786
|
|
|
758
787
|
|
|
759
788
|
def update_json(name: str, default: Any, mutator: Callable[[Any], Any]) -> Any:
|
|
@@ -767,6 +796,7 @@ def update_json(name: str, default: Any, mutator: Callable[[Any], Any]) -> Any:
|
|
|
767
796
|
current = _recover_json_unlocked(name, path, default)
|
|
768
797
|
result = mutator(current)
|
|
769
798
|
_atomic_write_unlocked(path, current)
|
|
799
|
+
_invalidate_read_cache(path)
|
|
770
800
|
return result
|
|
771
801
|
|
|
772
802
|
|
|
@@ -774,6 +804,7 @@ def write_json(name: str, data: Any) -> None:
|
|
|
774
804
|
"""Persist a state JSON file using :func:`atomic_write`."""
|
|
775
805
|
ensure_storage()
|
|
776
806
|
atomic_write(STATE / name, data)
|
|
807
|
+
_invalidate_read_cache(STATE / name)
|
|
777
808
|
|
|
778
809
|
|
|
779
810
|
def append_json(name: str, item: dict[str, Any]) -> dict[str, Any]:
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -262,16 +262,19 @@ const proxy = http.createServer((request, response) => {
|
|
|
262
262
|
headers: { ...request.headers, host: `127.0.0.1:${backendPort}`, "accept-encoding": "identity" },
|
|
263
263
|
}, (upstreamResponse) => {
|
|
264
264
|
const responseHeaders = { ...upstreamResponse.headers };
|
|
265
|
-
// Streamlit entrega HTML/JS de uma instância local que pode mudar após
|
|
266
|
-
// uma atualização via npx. Não permitir que o browser reaproveite uma
|
|
267
|
-
// página ou bundle da instância anterior.
|
|
268
|
-
delete responseHeaders.etag;
|
|
269
|
-
delete responseHeaders["last-modified"];
|
|
270
|
-
responseHeaders["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
|
|
271
|
-
responseHeaders.pragma = "no-cache";
|
|
272
|
-
responseHeaders.expires = "0";
|
|
273
265
|
const contentType = String(responseHeaders["content-type"] || "").toLowerCase();
|
|
274
266
|
const isHtml = contentType.includes("text/html") && !responseHeaders["content-encoding"];
|
|
267
|
+
// O documento HTML contém o estado da sessão e pode mudar após uma
|
|
268
|
+
// actualização via npx; os bundles estáticos do Streamlit, por outro lado,
|
|
269
|
+
// devem ser reutilizados pelo browser. Desactivar cache em todos os
|
|
270
|
+
// recursos tornava cada aba muito mais pesada e multiplicava downloads.
|
|
271
|
+
if (isHtml) {
|
|
272
|
+
delete responseHeaders.etag;
|
|
273
|
+
delete responseHeaders["last-modified"];
|
|
274
|
+
responseHeaders["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
|
|
275
|
+
responseHeaders.pragma = "no-cache";
|
|
276
|
+
responseHeaders.expires = "0";
|
|
277
|
+
}
|
|
275
278
|
if (!isHtml) {
|
|
276
279
|
response.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
277
280
|
upstreamResponse.pipe(response);
|