@danhachuel/thunderbolt 0.4.45 → 0.4.46
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 +93 -2
- package/hermes_ui/creative_generation.py +41 -0
- package/integrations/youtube_update.py +107 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -80,10 +80,11 @@ from hermes_ui.thumbnails import (
|
|
|
80
80
|
upload_thumbnail_image,
|
|
81
81
|
)
|
|
82
82
|
from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
|
|
83
|
-
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_description, generate_video_keywords
|
|
83
|
+
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_description, generate_video_keywords, generate_video_update_metadata
|
|
84
84
|
from hermes_ui.media_generation import MediaGenerationError, generate_image_for_card, generate_video_for_card
|
|
85
85
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
86
86
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
87
|
+
from integrations.youtube_update import YOUTUBE_UPDATE_VIDEO, YouTubeVideoUpdater
|
|
87
88
|
from integrations.postiz import PostizAdapter
|
|
88
89
|
from integrations.upload_post import UploadPostAdapter, UPLOAD_POST_PLATFORM_OPTIONS, normalize_upload_post_platforms
|
|
89
90
|
from integrations.bilibili_upload import BilibiliApiAdapter, BILIBILI_DEFAULT_TID, BILIBILI_VIDEO_EXTENSIONS, normalise_bilibili_api_cards
|
|
@@ -7590,6 +7591,94 @@ def render_metadata_cleaner():
|
|
|
7590
7591
|
st.download_button("Descarregar", data=output.read_bytes(), file_name=output.name, mime="video/*", key=f"metadata_history_{record.get('id')}")
|
|
7591
7592
|
|
|
7592
7593
|
|
|
7594
|
+
def render_update_youtube_videos():
|
|
7595
|
+
st.title("Update Youtube Vídeos")
|
|
7596
|
+
st.caption("Actualize título, descrição e thumbnail de vídeos publicados sem alterar o ficheiro de vídeo.")
|
|
7597
|
+
settings = read_json("settings.json", {})
|
|
7598
|
+
channels = [channel for channel in read_json("channels.json", []) if isinstance(channel, dict) and channel.get("platform", "youtube") != "tiktok" and channel.get("active", True)]
|
|
7599
|
+
if not channels:
|
|
7600
|
+
st.info("Cadastre pelo menos um canal YouTube activo antes de actualizar vídeos.")
|
|
7601
|
+
return
|
|
7602
|
+
channel = st.selectbox("Canal YouTube", channels, format_func=lambda item: item.get("name") or item.get("url") or "Canal sem nome", key="update_youtube_channel")
|
|
7603
|
+
updater = YouTubeVideoUpdater(settings, STORAGE)
|
|
7604
|
+
refresh_key = f"update_youtube_loaded_{channel.get('id', '')}"
|
|
7605
|
+
if st.button("Carregar vídeos do canal", type="primary", use_container_width=True, key="update_youtube_load") or refresh_key not in st.session_state:
|
|
7606
|
+
with st.spinner("A carregar vídeos publicados…"):
|
|
7607
|
+
result = updater.list_videos(channel, max_results=50)
|
|
7608
|
+
st.session_state[refresh_key] = result.data if result.ok else {"error": result.message, "videos": []}
|
|
7609
|
+
loaded = st.session_state.get(refresh_key, {})
|
|
7610
|
+
if loaded.get("error"):
|
|
7611
|
+
st.error(loaded["error"])
|
|
7612
|
+
return
|
|
7613
|
+
videos = loaded.get("videos", [])
|
|
7614
|
+
st.caption(f"{len(videos)} vídeo(s) carregado(s). Operação: `{YOUTUBE_UPDATE_VIDEO}`")
|
|
7615
|
+
if not videos:
|
|
7616
|
+
st.info("Não foram encontrados vídeos publicados para este canal.")
|
|
7617
|
+
return
|
|
7618
|
+
tasks = load_video_tasks_for_catalog()
|
|
7619
|
+
blueprint = blueprint_for_channel(channel)
|
|
7620
|
+
for video in videos:
|
|
7621
|
+
video_id = str(video.get("id") or "")
|
|
7622
|
+
card_key = f"update_youtube_{video_id}"
|
|
7623
|
+
with st.container(border=True):
|
|
7624
|
+
top = st.columns([1.2, 3.8, 1.2])
|
|
7625
|
+
with top[0]:
|
|
7626
|
+
if video.get("thumbnail_url"):
|
|
7627
|
+
st.image(video["thumbnail_url"], use_container_width=True)
|
|
7628
|
+
with top[1]:
|
|
7629
|
+
st.markdown(f"**{video.get('title') or 'Sem título'}**")
|
|
7630
|
+
st.caption(f"{video.get('published_at') or 'Data indisponível'} · {video.get('privacy_status') or 'estado desconhecido'} · `{video_id}`")
|
|
7631
|
+
st.markdown(f"[Abrir no YouTube]({video.get('url')})")
|
|
7632
|
+
with top[2]:
|
|
7633
|
+
st.caption("Alteração")
|
|
7634
|
+
st.write("Metadados")
|
|
7635
|
+
st.write("Sem alteração do vídeo")
|
|
7636
|
+
title_key, desc_key = f"{card_key}_title", f"{card_key}_description"
|
|
7637
|
+
title = st.text_input("Título", value=st.session_state.get(title_key, video.get("title", "")), key=title_key, max_chars=100)
|
|
7638
|
+
description = st.text_area("Descrição", value=st.session_state.get(desc_key, video.get("description", "")), key=desc_key, height=150)
|
|
7639
|
+
ai_cols = st.columns(3)
|
|
7640
|
+
script = next((str(task.get("script") or task.get("video_script") or "") for task in tasks if str(task.get("youtube_video_id") or task.get("video_id") or "") == video_id), "")
|
|
7641
|
+
with ai_cols[0]:
|
|
7642
|
+
if st.button("Gerar título", key=f"{card_key}_ai_title", use_container_width=True):
|
|
7643
|
+
try:
|
|
7644
|
+
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="title")
|
|
7645
|
+
st.session_state[title_key] = generated["title"]
|
|
7646
|
+
st.rerun()
|
|
7647
|
+
except CreativeGenerationError as exc:
|
|
7648
|
+
st.error(str(exc))
|
|
7649
|
+
with ai_cols[1]:
|
|
7650
|
+
if st.button("Gerar descrição", key=f"{card_key}_ai_description", use_container_width=True):
|
|
7651
|
+
try:
|
|
7652
|
+
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="description")
|
|
7653
|
+
st.session_state[desc_key] = generated["description"]
|
|
7654
|
+
st.rerun()
|
|
7655
|
+
except CreativeGenerationError as exc:
|
|
7656
|
+
st.error(str(exc))
|
|
7657
|
+
with ai_cols[2]:
|
|
7658
|
+
if st.button("Gerar título e descrição", key=f"{card_key}_ai_both", use_container_width=True):
|
|
7659
|
+
try:
|
|
7660
|
+
generated = generate_video_update_metadata(settings, channel, video, script=script, blueprint=blueprint, mode="both")
|
|
7661
|
+
st.session_state[title_key] = generated.get("title", title)
|
|
7662
|
+
st.session_state[desc_key] = generated.get("description", description)
|
|
7663
|
+
st.rerun()
|
|
7664
|
+
except CreativeGenerationError as exc:
|
|
7665
|
+
st.error(str(exc))
|
|
7666
|
+
thumbnail = st.file_uploader("Trocar thumbnail (opcional)", type=["jpg", "jpeg", "png"], key=f"{card_key}_thumbnail")
|
|
7667
|
+
if st.button("Actualizar no YouTube", type="primary", key=f"{card_key}_save", use_container_width=True):
|
|
7668
|
+
thumbnail_path = None
|
|
7669
|
+
if thumbnail is not None:
|
|
7670
|
+
thumbnail_path = STORAGE / "youtube_update" / f"{video_id}_{thumbnail.name}"
|
|
7671
|
+
thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
|
|
7672
|
+
thumbnail_path.write_bytes(thumbnail.getvalue())
|
|
7673
|
+
result = updater.update_video(video, title=title, description=description, thumbnail_path=thumbnail_path)
|
|
7674
|
+
if result.ok:
|
|
7675
|
+
st.success(f"{result.message} Campos: {', '.join(result.data.get('changed') or []) or 'nenhum'}.")
|
|
7676
|
+
st.session_state.pop(refresh_key, None)
|
|
7677
|
+
st.rerun()
|
|
7678
|
+
else:
|
|
7679
|
+
st.error(result.message)
|
|
7680
|
+
|
|
7681
|
+
|
|
7593
7682
|
def render_pipeline():
|
|
7594
7683
|
st.title("Pipeline Vídeos")
|
|
7595
7684
|
st.caption("Estado das filas locais e dependências da cascata")
|
|
@@ -7612,6 +7701,7 @@ def main():
|
|
|
7612
7701
|
("Roteiros", ":material/article:", "Roteiros"),
|
|
7613
7702
|
("Thumbnails", ":material/image:", "Thumbnails"),
|
|
7614
7703
|
("Upload", ":material/cloud_upload:", "Upload"),
|
|
7704
|
+
("Update Youtube Vídeos", ":material/edit_note:", "Update Youtube Vídeos"),
|
|
7615
7705
|
]
|
|
7616
7706
|
channel_profile_items = [
|
|
7617
7707
|
("Canais YouTube", ":material/ondemand_video:", "Canais YouTube"),
|
|
@@ -7699,7 +7789,7 @@ def main():
|
|
|
7699
7789
|
nav_paths = {
|
|
7700
7790
|
"Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok",
|
|
7701
7791
|
"Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
|
|
7702
|
-
"Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Criação de Shorts": "/pipeline-videos/shorts", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload",
|
|
7792
|
+
"Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Criação de Shorts": "/pipeline-videos/shorts", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload", "Update Youtube Vídeos": "/pipeline-videos/update-youtube",
|
|
7703
7793
|
"Pipeline Música": "/pipeline-musica", "Criação de Músicas": "/pipeline-musica/criacao", "Music Backlog": "/pipeline-musica/backlog", "Vozes Personalizadas": "/pipeline-musica/vozes-personalizadas", "Upload Música": "/pipeline-musica/upload",
|
|
7704
7794
|
"Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Canais Tiktok": "/canais-perfis-videos/canais-tiktok", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
|
|
7705
7795
|
"AI Influencers": "/ai-influencers", "Personagens": "/ai-influencers/personagens", "Geração de Conteúdo IA": "/ai-influencers/geracao-conteudo", "Motion Control": "/ai-influencers/motion-control", "UGC Products": "/ai-influencers/ugc-products", "Redes Sociais": "/ai-influencers/redes-sociais",
|
|
@@ -7786,6 +7876,7 @@ def main():
|
|
|
7786
7876
|
"Roteiros": render_scripts,
|
|
7787
7877
|
"Thumbnails": render_thumbnails,
|
|
7788
7878
|
"Upload": render_upload,
|
|
7879
|
+
"Update Youtube Vídeos": render_update_youtube_videos,
|
|
7789
7880
|
"Blueprints Youtube": render_blueprints,
|
|
7790
7881
|
"Thumbnail Blueprints": render_thumbnail_blueprints,
|
|
7791
7882
|
"Brandings Youtube": render_youtube_brandings,
|
|
@@ -599,3 +599,44 @@ def generate_ugc_segment_prompts(settings: dict[str, Any], instructions: str) ->
|
|
|
599
599
|
except CreativeGenerationError:
|
|
600
600
|
pass
|
|
601
601
|
return _ugc_fallback_prompts(clean)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def generate_video_update_metadata(
|
|
605
|
+
settings: dict[str, Any],
|
|
606
|
+
channel: dict[str, Any],
|
|
607
|
+
video: dict[str, Any],
|
|
608
|
+
*,
|
|
609
|
+
script: str = "",
|
|
610
|
+
blueprint: dict[str, Any] | None = None,
|
|
611
|
+
mode: str = "both",
|
|
612
|
+
) -> dict[str, str]:
|
|
613
|
+
"""Generate reviewed metadata for an already-published YouTube video."""
|
|
614
|
+
context = channel_context(channel, blueprint)
|
|
615
|
+
language_instruction = _language_instruction(video.get("default_language") or channel.get("language") or context["language"])
|
|
616
|
+
fields = {"title": "title", "description": "description"} if mode == "both" else {mode: mode}
|
|
617
|
+
system = (
|
|
618
|
+
"És um editor de metadados de YouTube. Gera apenas os campos solicitados em JSON válido. "
|
|
619
|
+
f"Escreve todos os campos em {language_instruction}. Mantém o tema, o roteiro e o nicho do canal, "
|
|
620
|
+
"evita clickbait falso, alegações não verificadas e menções a IA. O título deve ser claro e ter no máximo 100 caracteres; "
|
|
621
|
+
"a descrição deve ser natural, informativa e preservar factos fornecidos no roteiro."
|
|
622
|
+
)
|
|
623
|
+
user = json.dumps({
|
|
624
|
+
"requested_fields": list(fields.values()),
|
|
625
|
+
"channel": context,
|
|
626
|
+
"video": {"id": video.get("id"), "title": video.get("title", ""), "description": video.get("description", ""), "language": video.get("default_language", "")},
|
|
627
|
+
"script": str(script or "")[:12000],
|
|
628
|
+
"requirements": {"title_max_characters": 100, "description_max_characters": 1800},
|
|
629
|
+
}, ensure_ascii=False)
|
|
630
|
+
result = _chat_json(settings, system, user)
|
|
631
|
+
output: dict[str, str] = {}
|
|
632
|
+
if "title" in fields:
|
|
633
|
+
title = str(result.get("title") or "").strip()[:100]
|
|
634
|
+
if not title:
|
|
635
|
+
raise CreativeGenerationError("O provider não devolveu um título válido.")
|
|
636
|
+
output["title"] = title
|
|
637
|
+
if "description" in fields:
|
|
638
|
+
description = str(result.get("description") or "").strip()[:1800]
|
|
639
|
+
if not description:
|
|
640
|
+
raise CreativeGenerationError("O provider não devolveu uma descrição válida.")
|
|
641
|
+
output["description"] = description
|
|
642
|
+
return output
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from integrations.platforms import IntegrationResult
|
|
8
|
+
from integrations.youtube_upload import AGENT_SCOPES, YouTubeAutomationAgentUploader, _path_value
|
|
9
|
+
|
|
10
|
+
YOUTUBE_UPDATE_VIDEO = "YOUTUBE_UPDATE_VIDEO"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _channel_id(channel: dict[str, Any]) -> str:
|
|
14
|
+
value = str(channel.get("youtube_channel_id") or channel.get("youtube_id") or "").strip()
|
|
15
|
+
if value.startswith("UC"):
|
|
16
|
+
return value
|
|
17
|
+
match = re.search(r"(?:/channel/)(UC[A-Za-z0-9_-]+)", str(channel.get("url") or ""))
|
|
18
|
+
return match.group(1) if match else ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class YouTubeVideoUpdater(YouTubeAutomationAgentUploader):
|
|
22
|
+
"""Official OAuth adapter for metadata-only YouTube updates."""
|
|
23
|
+
|
|
24
|
+
def list_videos(self, channel: dict[str, Any], *, page_token: str = "", max_results: int = 25) -> IntegrationResult:
|
|
25
|
+
channel_id = _channel_id(channel)
|
|
26
|
+
if not channel_id:
|
|
27
|
+
return IntegrationResult(False, "Este canal não tem um YouTube channel ID. Actualize o cadastro do canal antes de listar vídeos.", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": []})
|
|
28
|
+
try:
|
|
29
|
+
credentials = self._load_credentials()
|
|
30
|
+
if credentials is None:
|
|
31
|
+
return IntegrationResult(False, "Autorize primeiro a conta YouTube para listar vídeos.", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": []})
|
|
32
|
+
youtube = self._build_client(credentials)
|
|
33
|
+
channel_response = youtube.channels().list(part="contentDetails", id=channel_id).execute()
|
|
34
|
+
items = channel_response.get("items") or []
|
|
35
|
+
if not items:
|
|
36
|
+
return IntegrationResult(False, "O canal YouTube não foi encontrado para esta conta autorizada.", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": []})
|
|
37
|
+
uploads_id = ((items[0].get("contentDetails") or {}).get("relatedPlaylists") or {}).get("uploads")
|
|
38
|
+
if not uploads_id:
|
|
39
|
+
return IntegrationResult(False, "Não foi possível localizar a playlist de uploads do canal.", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": []})
|
|
40
|
+
ids: list[str] = []
|
|
41
|
+
next_token = page_token or ""
|
|
42
|
+
while True:
|
|
43
|
+
playlist = youtube.playlistItems().list(part="snippet,contentDetails", playlistId=uploads_id, maxResults=max(1, min(50, int(max_results))), pageToken=next_token).execute()
|
|
44
|
+
ids.extend(str((item.get("contentDetails") or {}).get("videoId") or "") for item in playlist.get("items", []))
|
|
45
|
+
next_token = str(playlist.get("nextPageToken") or "")
|
|
46
|
+
if not next_token:
|
|
47
|
+
break
|
|
48
|
+
ids = [item for item in ids if item]
|
|
49
|
+
if not ids:
|
|
50
|
+
return IntegrationResult(True, "Nenhum vídeo encontrado neste canal.", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": [], "next_page_token": ""})
|
|
51
|
+
details = youtube.videos().list(part="snippet,status,contentDetails", id=",".join(ids)).execute()
|
|
52
|
+
videos = []
|
|
53
|
+
for item in details.get("items", []):
|
|
54
|
+
snippet = item.get("snippet") or {}
|
|
55
|
+
videos.append({
|
|
56
|
+
"id": item.get("id", ""),
|
|
57
|
+
"title": snippet.get("title", ""),
|
|
58
|
+
"description": snippet.get("description", ""),
|
|
59
|
+
"category_id": snippet.get("categoryId", "22"),
|
|
60
|
+
"default_language": snippet.get("defaultLanguage") or snippet.get("defaultAudioLanguage") or channel.get("language") or "pt",
|
|
61
|
+
"published_at": snippet.get("publishedAt", ""),
|
|
62
|
+
"channel_id": snippet.get("channelId", channel_id),
|
|
63
|
+
"thumbnail_url": ((snippet.get("thumbnails") or {}).get("high") or (snippet.get("thumbnails") or {}).get("default") or {}).get("url", ""),
|
|
64
|
+
"privacy_status": (item.get("status") or {}).get("privacyStatus", ""),
|
|
65
|
+
"url": f"https://www.youtube.com/watch?v={item.get('id', '')}",
|
|
66
|
+
})
|
|
67
|
+
return IntegrationResult(True, f"{len(videos)} vídeo(s) carregado(s).", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": videos, "next_page_token": ""})
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
return IntegrationResult(False, f"Falha ao listar vídeos YouTube: {exc}", {"operation": YOUTUBE_UPDATE_VIDEO, "videos": []})
|
|
70
|
+
|
|
71
|
+
def update_video(self, video: dict[str, Any], *, title: str, description: str, thumbnail_path: str | Path | None = None) -> IntegrationResult:
|
|
72
|
+
video_id = str(video.get("id") or "").strip()
|
|
73
|
+
if not video_id:
|
|
74
|
+
return IntegrationResult(False, "Vídeo sem videoId; a actualização foi recusada.", {"operation": YOUTUBE_UPDATE_VIDEO})
|
|
75
|
+
title = str(title or "").strip()
|
|
76
|
+
description = str(description or "")
|
|
77
|
+
if not title:
|
|
78
|
+
return IntegrationResult(False, "O título não pode ficar vazio.", {"operation": YOUTUBE_UPDATE_VIDEO, "video_id": video_id})
|
|
79
|
+
changed: list[str] = []
|
|
80
|
+
try:
|
|
81
|
+
credentials = self._load_credentials()
|
|
82
|
+
if credentials is None:
|
|
83
|
+
return IntegrationResult(False, "Autorize primeiro a conta YouTube.", {"operation": YOUTUBE_UPDATE_VIDEO, "video_id": video_id})
|
|
84
|
+
youtube = self._build_client(credentials)
|
|
85
|
+
current_category = str(video.get("category_id") or "22")
|
|
86
|
+
body = {"id": video_id, "snippet": {"title": title[:100], "description": description, "categoryId": current_category}}
|
|
87
|
+
if title != str(video.get("title") or "") or description != str(video.get("description") or ""):
|
|
88
|
+
youtube.videos().update(part="snippet", body=body).execute()
|
|
89
|
+
if title != str(video.get("title") or ""):
|
|
90
|
+
changed.append("title")
|
|
91
|
+
if description != str(video.get("description") or ""):
|
|
92
|
+
changed.append("description")
|
|
93
|
+
thumbnail = _path_value(thumbnail_path)
|
|
94
|
+
if thumbnail:
|
|
95
|
+
if not thumbnail.exists() or not thumbnail.is_file():
|
|
96
|
+
return IntegrationResult(False, "A thumbnail seleccionada não existe.", {"operation": YOUTUBE_UPDATE_VIDEO, "video_id": video_id, "changed": changed})
|
|
97
|
+
with thumbnail.open("rb") as handle:
|
|
98
|
+
youtube.thumbnails().set(videoId=video_id, media_body=handle).execute()
|
|
99
|
+
changed.append("thumbnail")
|
|
100
|
+
return IntegrationResult(True, "Vídeo actualizado sem alterar o ficheiro de vídeo.", {"operation": YOUTUBE_UPDATE_VIDEO, "video_id": video_id, "changed": changed, "url": f"https://www.youtube.com/watch?v={video_id}"})
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
return IntegrationResult(False, f"Falha ao actualizar o vídeo YouTube: {exc}", {"operation": YOUTUBE_UPDATE_VIDEO, "video_id": video_id, "changed": changed})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def update_youtube_video(settings: dict[str, Any], channel: dict[str, Any], video: dict[str, Any], *, title: str, description: str, thumbnail_path: str | Path | None = None) -> IntegrationResult:
|
|
106
|
+
updater = YouTubeVideoUpdater(settings, Path(__file__).resolve().parents[1] / "storage")
|
|
107
|
+
return updater.update_video(video, title=title, description=description, thumbnail_path=thumbnail_path)
|
package/package.json
CHANGED