@danhachuel/thunderbolt 0.7.7 → 0.7.9
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 +98 -110
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -3088,6 +3088,18 @@ def render_channels():
|
|
|
3088
3088
|
edit_key = f"edit_channel_{channel_id}"
|
|
3089
3089
|
with st.container(border=True):
|
|
3090
3090
|
header_cols = st.columns([0.7, 2.75, 1.1, 1.0, 1.35, 1.35])
|
|
3091
|
+
channel_url = str(channel.get("url") or "").strip()
|
|
3092
|
+
if not channel_url:
|
|
3093
|
+
channel_id_or_handle = str(channel.get("youtube_channel_id") or channel.get("youtube_id") or channel.get("handle") or "").strip()
|
|
3094
|
+
if channel_id_or_handle:
|
|
3095
|
+
if channel_id_or_handle.startswith("http"):
|
|
3096
|
+
channel_url = channel_id_or_handle
|
|
3097
|
+
elif channel_id_or_handle.startswith("UC"):
|
|
3098
|
+
channel_url = f"https://www.youtube.com/channel/{channel_id_or_handle}"
|
|
3099
|
+
elif channel_id_or_handle.startswith(("@", "channel/")):
|
|
3100
|
+
channel_url = f"https://www.youtube.com/{channel_id_or_handle}"
|
|
3101
|
+
else:
|
|
3102
|
+
channel_url = f"https://www.youtube.com/@{channel_id_or_handle}"
|
|
3091
3103
|
with header_cols[0]:
|
|
3092
3104
|
if channel.get("thumbnail_url"):
|
|
3093
3105
|
st.image(channel["thumbnail_url"], width=64)
|
|
@@ -3104,6 +3116,8 @@ def render_channels():
|
|
|
3104
3116
|
with header_cols[4]:
|
|
3105
3117
|
st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
|
|
3106
3118
|
with header_cols[5]:
|
|
3119
|
+
if channel_url:
|
|
3120
|
+
st.link_button("Abrir canal", channel_url, type="primary", width="content")
|
|
3107
3121
|
if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações", width="stretch"):
|
|
3108
3122
|
with st.spinner("A actualizar métricas YouTube…"):
|
|
3109
3123
|
refreshed, message = _refresh_youtube_channel_metrics(channel, youtube)
|
|
@@ -7072,33 +7086,27 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7072
7086
|
missing_document_parts = list(direct_status.get("missing_cookies", []))
|
|
7073
7087
|
if not direct_status.get("has_session_info"):
|
|
7074
7088
|
missing_document_parts.append("sessionInfo")
|
|
7075
|
-
|
|
7089
|
+
google_ready = bool(
|
|
7076
7090
|
account_email_snapshot != "sem e-mail"
|
|
7077
7091
|
and str(batch_account.get("client_id") or "").strip()
|
|
7078
7092
|
and str(batch_account.get("client_secret") or "").strip()
|
|
7079
|
-
and bool(direct_status.get("document_exists"))
|
|
7080
|
-
and not missing_document_parts
|
|
7081
7093
|
)
|
|
7094
|
+
youtube_upload_ready = bool(direct_status.get("document_exists") and not missing_document_parts)
|
|
7095
|
+
google_status_label = "Configured" if google_ready else ("Missing key" if not str(batch_account.get("client_id") or "").strip() or not str(batch_account.get("client_secret") or "").strip() else "Missing configuration")
|
|
7096
|
+
youtube_status_label = "Configured" if youtube_upload_ready else ("Missing key" if not direct_status.get("has_session_info") else "Missing configuration")
|
|
7082
7097
|
if missing_document_parts:
|
|
7083
7098
|
youtube_accounts_missing_document.append(account_email_snapshot)
|
|
7084
7099
|
|
|
7085
7100
|
with st.container(border=True):
|
|
7086
|
-
account_header_cols = st.columns([
|
|
7101
|
+
account_header_cols = st.columns([2.35, 1.15, 1.35])
|
|
7087
7102
|
with account_header_cols[0]:
|
|
7088
7103
|
st.subheader(f"{account_label_snapshot} — {account_email_snapshot}")
|
|
7089
7104
|
with account_header_cols[1]:
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
st.caption(
|
|
7094
|
-
|
|
7095
|
-
st.warning(health_message, icon="⚠️")
|
|
7096
|
-
elif session_health.status == "expired":
|
|
7097
|
-
st.error(health_message, icon="⚠️")
|
|
7098
|
-
elif session_health.status == "unknown":
|
|
7099
|
-
st.warning(health_message, icon="⚠️")
|
|
7100
|
-
elif session_health.status in {"blocked_by_google", "invalid_format"}:
|
|
7101
|
-
st.error(health_message, icon="⚠️")
|
|
7105
|
+
st.caption("Conta Google")
|
|
7106
|
+
_api_status_badge(google_status_label, "ready" if google_ready else "missing")
|
|
7107
|
+
with account_header_cols[2]:
|
|
7108
|
+
st.caption("YouTube-Video-Upload-Frontend-Api")
|
|
7109
|
+
_api_status_badge(youtube_status_label, "ready" if youtube_upload_ready else "missing")
|
|
7102
7110
|
with st.expander("Detalhes da conta Google", expanded=False):
|
|
7103
7111
|
with st.form(f"batch_account_form_{account_id}"):
|
|
7104
7112
|
account_cols = st.columns(2)
|
|
@@ -7108,48 +7116,11 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7108
7116
|
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
7109
7117
|
with account_cols[1]:
|
|
7110
7118
|
account_client_secret = st.text_input("OAuth Client Secret", value=str(batch_account.get("client_secret", "")), type="password", key=f"batch_client_secret_{account_id}")
|
|
7111
|
-
account_session_info = st.text_input(
|
|
7112
|
-
"sessionInfo token desta conta Google",
|
|
7113
|
-
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
7114
|
-
type="password",
|
|
7115
|
-
key=f"batch_session_info_{account_id}",
|
|
7116
|
-
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
|
|
7117
|
-
)
|
|
7118
|
-
captured_at_value = str(
|
|
7119
|
-
session_health.captured_at
|
|
7120
|
-
or batch_account.get("sessionInfoCapturedAt")
|
|
7121
|
-
or batch_account.get("session_info_captured_at")
|
|
7122
|
-
or ""
|
|
7123
|
-
).strip()
|
|
7124
|
-
try:
|
|
7125
|
-
account_session_info_captured_at = datetime.fromisoformat(captured_at_value.replace("Z", "+00:00")).date() if captured_at_value else None
|
|
7126
|
-
except ValueError:
|
|
7127
|
-
account_session_info_captured_at = None
|
|
7128
|
-
account_session_info_captured_at = st.date_input(
|
|
7129
|
-
"Data de Captura",
|
|
7130
|
-
value=account_session_info_captured_at,
|
|
7131
|
-
key=f"batch_session_info_captured_at_{account_id}",
|
|
7132
|
-
help="Data em que o sessionInfo token foi capturado. É usada para calcular o alerta de expiração e não revela o token.",
|
|
7133
|
-
)
|
|
7134
7119
|
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", width="stretch")
|
|
7135
7120
|
|
|
7136
|
-
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
7137
|
-
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
7138
|
-
document_upload = st.file_uploader(
|
|
7139
|
-
"Subir documento de cookies/credenciais",
|
|
7140
|
-
type=["json"],
|
|
7141
|
-
key=f"direct_credentials_document_{account_id}",
|
|
7142
|
-
help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.",
|
|
7143
|
-
)
|
|
7144
|
-
if missing_document_parts:
|
|
7145
|
-
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
7146
|
-
else:
|
|
7147
|
-
st.success("Documento completo para a conta Google")
|
|
7148
|
-
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
7149
|
-
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", width="stretch")
|
|
7150
7121
|
|
|
7151
7122
|
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
7152
|
-
status_cols = st.columns(
|
|
7123
|
+
status_cols = st.columns(2)
|
|
7153
7124
|
with status_cols[0]:
|
|
7154
7125
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
7155
7126
|
with status_cols[1]:
|
|
@@ -7158,26 +7129,19 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7158
7129
|
(st.success if result.ok else st.error)(result.message)
|
|
7159
7130
|
if result.ok:
|
|
7160
7131
|
st.rerun()
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
7175
|
-
for channel in channel_state:
|
|
7176
|
-
if str(channel.get("google_account_id") or "") == account_id:
|
|
7177
|
-
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
7178
|
-
write_json("channels.json", channel_state)
|
|
7179
|
-
write_json("settings.json", settings)
|
|
7180
|
-
st.rerun()
|
|
7132
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", width="stretch"):
|
|
7133
|
+
delete_youtube_batch_token(batch_account, STORAGE)
|
|
7134
|
+
delete_credentials_document(STORAGE, batch_account)
|
|
7135
|
+
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
7136
|
+
settings["youtube_batch_accounts"] = remaining_accounts
|
|
7137
|
+
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
7138
|
+
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
7139
|
+
for channel in channel_state:
|
|
7140
|
+
if str(channel.get("google_account_id") or "") == account_id:
|
|
7141
|
+
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
7142
|
+
write_json("channels.json", channel_state)
|
|
7143
|
+
write_json("settings.json", settings)
|
|
7144
|
+
st.rerun()
|
|
7181
7145
|
|
|
7182
7146
|
if save_account:
|
|
7183
7147
|
if "@" not in account_email.strip():
|
|
@@ -7190,61 +7154,85 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
7190
7154
|
credentials_changed = any(existing.get(field, "") != value for field, value in (("email", account_email.strip()), ("client_id", account_client_id.strip()), ("client_secret", account_client_secret.strip())))
|
|
7191
7155
|
if credentials_changed:
|
|
7192
7156
|
delete_youtube_batch_token(existing, STORAGE)
|
|
7193
|
-
|
|
7194
|
-
existing.update({"label": account_label.strip() or "Canais YouTube", "email": account_email.strip(), "client_id": account_client_id.strip(), "client_secret": account_client_secret.strip(), "sessionInfo": account_session_info.strip(), "sessionInfoCapturedAt": captured_at_iso})
|
|
7195
|
-
update_credentials_document_session_info(
|
|
7196
|
-
STORAGE,
|
|
7197
|
-
existing,
|
|
7198
|
-
account_session_info.strip(),
|
|
7199
|
-
captured_at=captured_at_iso,
|
|
7200
|
-
)
|
|
7157
|
+
existing.update({"label": account_label.strip() or "Canais YouTube", "email": account_email.strip(), "client_id": account_client_id.strip(), "client_secret": account_client_secret.strip()})
|
|
7201
7158
|
ensure_credentials_document(STORAGE, existing, settings, channel_state)
|
|
7202
7159
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
7203
7160
|
write_json("settings.json", settings)
|
|
7204
|
-
st.success("
|
|
7161
|
+
st.success("Dados da conta Google guardados.")
|
|
7205
7162
|
st.rerun()
|
|
7206
7163
|
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7164
|
+
with st.expander("Detalhes YouTube-Video-Upload-Frontend-Api", expanded=False):
|
|
7165
|
+
health_message = f"SessionInfo: {session_health.message}"
|
|
7166
|
+
if session_health.status == "healthy":
|
|
7167
|
+
st.success(health_message)
|
|
7168
|
+
elif session_health.status in {"expiring", "unknown"}:
|
|
7169
|
+
st.warning(health_message, icon="⚠️")
|
|
7170
|
+
elif session_health.status in {"expired", "blocked_by_google", "invalid_format"}:
|
|
7171
|
+
st.error(health_message, icon="⚠️")
|
|
7172
|
+
account_session_info = st.text_input(
|
|
7173
|
+
"sessionInfo token desta conta Google",
|
|
7174
|
+
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
7175
|
+
type="password",
|
|
7176
|
+
key=f"batch_session_info_{account_id}",
|
|
7177
|
+
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json.",
|
|
7178
|
+
)
|
|
7179
|
+
captured_at_value = str(session_health.captured_at or batch_account.get("sessionInfoCapturedAt") or batch_account.get("session_info_captured_at") or "").strip()
|
|
7180
|
+
try:
|
|
7181
|
+
account_session_info_captured_at = datetime.fromisoformat(captured_at_value.replace("Z", "+00:00")).date() if captured_at_value else None
|
|
7182
|
+
except ValueError:
|
|
7183
|
+
account_session_info_captured_at = None
|
|
7184
|
+
account_session_info_captured_at = st.date_input("Data de Captura", value=account_session_info_captured_at, key=f"batch_session_info_captured_at_{account_id}", help="Data em que o sessionInfo token foi capturado.")
|
|
7185
|
+
st.markdown("**Área de Upload dos cookies**")
|
|
7186
|
+
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
7187
|
+
document_upload = st.file_uploader("Subir documento de cookies/credenciais", type=["json"], key=f"direct_credentials_document_{account_id}", help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.")
|
|
7188
|
+
if missing_document_parts:
|
|
7189
|
+
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
7190
|
+
else:
|
|
7191
|
+
st.success("Documento completo para a conta Google")
|
|
7192
|
+
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
7193
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", width="stretch")
|
|
7194
|
+
if st.button("Autorizar upload YouTube", key=f"batch_authorize_upload_{account_id}", width="stretch", help="Autoriza esta conta com o escopo de publicação YouTube."):
|
|
7195
|
+
result = authorize_youtube_agent(settings, STORAGE, account=batch_account)
|
|
7196
|
+
(st.success if result.ok else st.error)(result.message)
|
|
7197
|
+
if result.ok:
|
|
7198
|
+
st.rerun()
|
|
7225
7199
|
st.markdown("**Renovação de sessionInfo e cookies**")
|
|
7226
7200
|
st.caption("A renovação é manual ou opt-in antes do upload; não há timer residente nem execução ao seleccionar a conta.")
|
|
7227
7201
|
renewal_cols = st.columns([2, 1, 1])
|
|
7228
|
-
auto_renew = renewal_cols[0].checkbox(
|
|
7229
|
-
"Renovar automaticamente antes do upload",
|
|
7230
|
-
value=bool(batch_account.get("auto_renew_before_upload", False)),
|
|
7231
|
-
key=f"auto_renew_before_upload_{account_id}",
|
|
7232
|
-
)
|
|
7202
|
+
auto_renew = renewal_cols[0].checkbox("Renovar automaticamente antes do upload", value=bool(batch_account.get("auto_renew_before_upload", False)), key=f"auto_renew_before_upload_{account_id}")
|
|
7233
7203
|
if renewal_cols[1].button("Renovar agora", key=f"renew_session_info_{account_id}", type="primary", width="stretch"):
|
|
7234
7204
|
result = renew_account_session(STORAGE, batch_account, settings, force=True, wait_seconds=10)
|
|
7235
7205
|
(st.success if result.ok else st.error)(result.message)
|
|
7236
7206
|
if result.ok:
|
|
7237
7207
|
st.rerun()
|
|
7238
7208
|
with renewal_cols[2]:
|
|
7239
|
-
_api_status_badge(
|
|
7240
|
-
"Configured" if session_health.status == "healthy" else ("Missing key" if session_health.status == "missing" else "Missing configuration"),
|
|
7241
|
-
"ready" if session_health.status == "healthy" else "missing",
|
|
7242
|
-
)
|
|
7209
|
+
_api_status_badge("Configured" if session_health.status == "healthy" else ("Missing key" if session_health.status == "missing" else "Missing configuration"), "ready" if session_health.status == "healthy" else "missing")
|
|
7243
7210
|
if auto_renew != bool(batch_account.get("auto_renew_before_upload", False)):
|
|
7244
7211
|
batch_account["auto_renew_before_upload"] = bool(auto_renew)
|
|
7245
7212
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
7246
7213
|
write_json("settings.json", settings)
|
|
7247
7214
|
st.success("Preferência de renovação guardada para esta conta.")
|
|
7215
|
+
if document_save:
|
|
7216
|
+
if document_upload is None:
|
|
7217
|
+
st.error("Seleccione um documento JSON de cookies/credenciais antes de guardar.")
|
|
7218
|
+
else:
|
|
7219
|
+
try:
|
|
7220
|
+
merge_credentials_document(STORAGE, batch_account, document_upload.getvalue(), document_upload.name, session_info_override=account_session_info.strip(), channels=channel_state)
|
|
7221
|
+
update_credentials_document_session_info(STORAGE, batch_account, account_session_info.strip(), captured_at=account_session_info_captured_at.isoformat() if isinstance(account_session_info_captured_at, date) else "")
|
|
7222
|
+
st.success("Documento incorporado e guardado nesta conta Google.")
|
|
7223
|
+
st.rerun()
|
|
7224
|
+
except ValueError as exc:
|
|
7225
|
+
st.error(str(exc))
|
|
7226
|
+
if st.button("Guardar dados YouTube-Video-Upload-Frontend-Api", key=f"save_upload_api_data_{account_id}", width="stretch"):
|
|
7227
|
+
captured_at_iso = account_session_info_captured_at.isoformat() if isinstance(account_session_info_captured_at, date) else ""
|
|
7228
|
+
update_credentials_document_session_info(STORAGE, batch_account, account_session_info.strip(), captured_at=captured_at_iso)
|
|
7229
|
+
for existing in batch_accounts:
|
|
7230
|
+
if str(existing.get("id")) == account_id:
|
|
7231
|
+
existing.update({"sessionInfo": account_session_info.strip(), "sessionInfoCapturedAt": captured_at_iso})
|
|
7232
|
+
settings["youtube_batch_accounts"] = batch_accounts
|
|
7233
|
+
write_json("settings.json", settings)
|
|
7234
|
+
st.success("Dados YouTube-Video-Upload-Frontend-Api guardados.")
|
|
7235
|
+
st.rerun()
|
|
7248
7236
|
|
|
7249
7237
|
if youtube_accounts_missing_document:
|
|
7250
7238
|
st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
|
package/package.json
CHANGED