infografia_brief: slug y sitio opcionales
Escribir un slug de 40 caracteres y acordarse del --site era casi toda la
fricción de la herramienta, y el caso normal es siempre el mismo: quiero la
infografía del borrador que acabo de terminar.
infografia → el borrador más recién tocado de los dos sitios
infografia --lista → los enumera, con si tienen imagen ya o no
infografia <slug> → busca en los dos sitios; los slugs no se repiten
entre blogs, así que no hay ambigüedad posible
infografia <URL> → el sitio sale del dominio (pegada del navegador)
--site sigue existiendo, ahora solo para desambiguar o para acotar la búsqueda
de borradores a un blog.
Solo se ofrecen borradores al elegir automáticamente: un post publicado ya
tiene su imagen, y pedir el guion de uno publicado es la excepción, para la que
está pasar el slug o la URL a mano.
_post_list vuelca a fichero y no a PIPE, copiando el patrón de seo_watch y
remate_watch: un PIPE se trunca a 64 KB en silencio y con --formats html los
cuerpos son largos.
Verificado: las dos formas de URL (EN en www, ES en apex) resuelven su sitio,
el slug suelto encuentra el blog correcto, el slug inexistente sale con un
error claro, y la selección automática coge el más reciente y avisa de cuántos
más hay. El caso "no hay borradores" sale limpio en vez de reventar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+108
-12
@@ -40,9 +40,9 @@ TEMPLATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "infografia_
|
||||
# Mismos dos sitios que el resto de ~/seo-tools. El idioma NO se adivina del
|
||||
# texto: lo fija el sitio, que es un dato duro y no se equivoca nunca.
|
||||
SITES = {
|
||||
"en": {"cli": "ghst-en", "idioma": "inglés",
|
||||
"en": {"cli": "ghst-en", "host": "theexclusionzone.com", "idioma": "inglés",
|
||||
"fecha": "November 12, 1976", "decimal": "punto"},
|
||||
"es": {"cli": "ghst-es", "idioma": "español",
|
||||
"es": {"cli": "ghst-es", "host": "zonadeexclusion.com", "idioma": "español",
|
||||
"fecha": "12 de noviembre de 1976", "decimal": "coma"},
|
||||
}
|
||||
|
||||
@@ -85,21 +85,109 @@ def sh(cmd, timeout=600):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
|
||||
def fetch(slug, cli):
|
||||
"""Baja el post por el wrapper ghst-*, igual que seo_finish.fetch."""
|
||||
def _post_list(cli, extra="", timeout=300):
|
||||
"""Vuelca `ghst-* post list` a fichero y lo parsea.
|
||||
|
||||
A fichero y no a PIPE, copiando el patrón de seo_watch/remate_watch: un PIPE
|
||||
se trunca a 64 KB en silencio, y con --formats html los cuerpos son largos.
|
||||
"""
|
||||
fd, path = tempfile.mkstemp(suffix=".json", prefix="infografia_")
|
||||
os.close(fd)
|
||||
try:
|
||||
sh(f"{cli} post list --limit 100 --formats html --json > {path}")
|
||||
r = sh(f"{cli} post list --limit all {extra} --json > {path}", timeout=timeout)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"{cli} post list falló: {r.stderr.strip()[:200]}")
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
posts = data.get("posts", data) if isinstance(data, dict) else data
|
||||
return data.get("posts", data) if isinstance(data, dict) else data
|
||||
finally:
|
||||
os.unlink(path)
|
||||
for p in posts:
|
||||
|
||||
|
||||
def fetch(slug, cli):
|
||||
"""Baja el post por el wrapper ghst-*, igual que seo_finish.fetch."""
|
||||
for p in _post_list(cli, "--formats html"):
|
||||
if p.get("slug") == slug:
|
||||
return p
|
||||
sys.exit(f"ERROR: no encuentro el post '{slug}' en {cli}")
|
||||
return None
|
||||
|
||||
|
||||
def candidatos(sites=("es", "en")):
|
||||
"""Borradores de los dos sitios, del más recién tocado al más viejo.
|
||||
|
||||
Solo borradores: un post publicado ya tiene su imagen, y pedir el guion de
|
||||
algo publicado es la excepción (para eso está pasar el slug a mano).
|
||||
"""
|
||||
out = []
|
||||
for s in sites:
|
||||
try:
|
||||
posts = _post_list(SITES[s]["cli"],
|
||||
'--filter "status:draft" --fields id,slug,title,status,feature_image,updated_at')
|
||||
except Exception as e:
|
||||
print(f" (aviso: no pude listar {s}: {e})")
|
||||
continue
|
||||
for p in posts:
|
||||
if p.get("status") == "draft":
|
||||
p["_site"] = s
|
||||
out.append(p)
|
||||
return sorted(out, key=lambda p: p.get("updated_at") or "", reverse=True)
|
||||
|
||||
|
||||
def de_url(texto):
|
||||
"""Saca (site, slug) de una URL pegada del navegador. None si no lo es."""
|
||||
m = re.match(r"https?://(?:www\.)?([\w.-]+)/([\w-]+)/?", texto.strip())
|
||||
if not m:
|
||||
return None
|
||||
host, slug = m.group(1), m.group(2)
|
||||
for s, cfg in SITES.items():
|
||||
if cfg["host"] in host:
|
||||
return s, slug
|
||||
return None
|
||||
|
||||
|
||||
def elige(arg_slug, arg_site):
|
||||
"""Resuelve qué post hay que procesar. Devuelve (site, slug).
|
||||
|
||||
Tres formas de pedirlo, de más cómoda a más explícita:
|
||||
· sin argumentos → el borrador más recién tocado de los dos sitios
|
||||
· una URL pegada → el sitio sale del dominio
|
||||
· slug [--site] → si no se da el sitio, se busca en los dos
|
||||
"""
|
||||
if arg_slug:
|
||||
url = de_url(arg_slug)
|
||||
if url:
|
||||
return url
|
||||
if arg_site:
|
||||
return arg_site, arg_slug
|
||||
# Slug suelto: mirar en los dos sitios. Los slugs no se repiten entre
|
||||
# blogs, así que si aparece en uno solo no hay ambigüedad posible.
|
||||
hallados = [s for s in SITES if fetch(arg_slug, SITES[s]["cli"])]
|
||||
if len(hallados) == 1:
|
||||
return hallados[0], arg_slug
|
||||
if not hallados:
|
||||
sys.exit(f"ERROR: el slug '{arg_slug}' no está ni en ES ni en EN")
|
||||
sys.exit(f"ERROR: '{arg_slug}' existe en {hallados}; desambigua con --site")
|
||||
|
||||
borradores = candidatos([arg_site] if arg_site else ("es", "en"))
|
||||
if not borradores:
|
||||
sys.exit("No hay borradores. Pasa un slug o una URL si quieres el guion de un publicado.")
|
||||
p = borradores[0]
|
||||
print(f" sin slug: cojo el borrador más reciente → [{p['_site'].upper()}] {p['title'][:60]}")
|
||||
if len(borradores) > 1:
|
||||
print(f" (hay {len(borradores)} borradores; `infografia --lista` para ver los demás)")
|
||||
return p["_site"], p["slug"]
|
||||
|
||||
|
||||
def cmd_lista(site):
|
||||
borradores = candidatos([site] if site else ("es", "en"))
|
||||
if not borradores:
|
||||
print("No hay borradores en ninguno de los dos sitios.")
|
||||
return
|
||||
print(f"─── borradores ({len(borradores)}), del más recién tocado ───")
|
||||
for p in borradores:
|
||||
img = "con imagen" if p.get("feature_image") else "SIN imagen"
|
||||
print(f" [{p['_site'].upper()}] {str(p.get('updated_at'))[:16].replace('T',' ')} {img:11} {p['title'][:58]}")
|
||||
print(f" {p['slug']}")
|
||||
|
||||
|
||||
def plano(html):
|
||||
@@ -236,15 +324,23 @@ def formatea(guion):
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Guion verificado para la infografía de un post")
|
||||
ap.add_argument("slug")
|
||||
ap.add_argument("--site", choices=("en", "es"), required=True)
|
||||
ap.add_argument("slug", nargs="?", help="slug o URL; si se omite, el borrador más reciente")
|
||||
ap.add_argument("--site", choices=("en", "es"), help="normalmente se deduce solo")
|
||||
ap.add_argument("--lista", action="store_true", help="enumerar los borradores y salir")
|
||||
ap.add_argument("--out", help="guardar el prompt final aquí (además de imprimirlo)")
|
||||
ap.add_argument("--claude-bin", default=os.environ.get("CLAUDE_BIN", "claude"))
|
||||
ap.add_argument("--guion-json", help="saltarse el modelo y usar este guion ya extraído")
|
||||
a = ap.parse_args()
|
||||
|
||||
cfg = SITES[a.site]
|
||||
post = fetch(a.slug, cfg["cli"])
|
||||
if a.lista:
|
||||
return cmd_lista(a.site)
|
||||
|
||||
site, slug = elige(a.slug, a.site)
|
||||
a.site, a.slug = site, slug
|
||||
cfg = SITES[site]
|
||||
post = fetch(slug, cfg["cli"])
|
||||
if not post:
|
||||
sys.exit(f"ERROR: no encuentro el post '{slug}' en {cfg['cli']}")
|
||||
articulo = plano(post.get("html"))
|
||||
if len(articulo.split()) < 200:
|
||||
sys.exit(f"ERROR: el artículo tiene {len(articulo.split())} palabras; ¿seguro que es el slug bueno?")
|
||||
|
||||
Reference in New Issue
Block a user