@danhachuel/thunderbolt 0.7.59 → 0.7.60

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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.59",
3
+ "version": "0.7.60",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",