102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
import xml.etree.ElementTree as ET
|
|
import json
|
|
import argparse
|
|
import sys
|
|
import os
|
|
from tkinter import filedialog, ttk, Tk, messagebox
|
|
|
|
def escapar_markdown(texto):
|
|
if not texto: return ""
|
|
return texto.replace("|", "\\|").replace("[", "\\[").replace("]", "\\]")
|
|
|
|
def procesar_feeds(ruta_entrada, formato_salida):
|
|
try:
|
|
tree = ET.parse(ruta_entrada)
|
|
root = tree.getroot()
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
|
|
feeds = []
|
|
for outline in root.findall(".//outline[@xmlUrl]"):
|
|
feeds.append({
|
|
"titulo": outline.get('title') or outline.get('text') or "Sin título",
|
|
"url_web": outline.get('htmlUrl') or "#",
|
|
"url_rss": outline.get('xmlUrl') or "#"
|
|
})
|
|
|
|
nombre_base = os.path.splitext(ruta_entrada)[0]
|
|
|
|
# Con formatos integrados inline para evitar dramas de etiquetas prohibidas.
|
|
if formato_salida == 'html':
|
|
# Contenedor principal con CSS Inline
|
|
output = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; font-family: sans-serif; margin: 20px 0;">'
|
|
|
|
for f in feeds:
|
|
# Estilos directos en cada tarjeta y enlace
|
|
card_style = 'border: 1px solid #e1e4e8; border-radius: 12px; padding: 15px; display: flex; justify-content: space-between; align-items: center; background: #ffffff; box-shadow: 0 2px 4px rgba(0,0,0,0.05);'
|
|
title_style = 'font-weight: bold; color: #0366d6; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;'
|
|
rss_style = 'background: #f68140; color: #ffffff !important; padding: 5px 10px; border-radius: 6px; font-size: 12px; text-decoration: none; font-weight: bold; margin-left: 12px;'
|
|
|
|
output += f'\n <div style="{card_style}">'
|
|
output += f'\n <a href="{f["url_web"]}" style="{title_style}" target="_blank">{f["titulo"]}</a>'
|
|
output += f'\n <a href="{f["url_rss"]}" style="{rss_style}" target="_blank">RSS</a>'
|
|
output += f'\n </div>'
|
|
|
|
output += "\n</div>"
|
|
ext = ".html"
|
|
|
|
elif formato_salida == 'markdown':
|
|
output = "| Sitio Web | Feed RSS |\n| :--- | :---: |\n"
|
|
for f in feeds:
|
|
titulo_limpio = escapar_markdown(f['titulo'])
|
|
output += f"| [{titulo_limpio}]({f['url_web']}) | [RSS]({f['url_rss']}) |\n"
|
|
ext = ".md"
|
|
|
|
elif formato_salida == 'json':
|
|
output = json.dumps(feeds, indent=4, ensure_ascii=False)
|
|
ext = ".json"
|
|
|
|
archivo_final = nombre_base + ext
|
|
with open(archivo_final, "w", encoding="utf-8") as f:
|
|
f.write(output)
|
|
return archivo_final
|
|
|
|
def iniciar_gui():
|
|
root = Tk()
|
|
root.title("OPML-to-BlogRoll")
|
|
root.geometry("400x250")
|
|
archivo_ruta = {"path": ""}
|
|
|
|
def seleccionar():
|
|
path = filedialog.askopenfilename(filetypes=[("Feeds", "*.opml *.xml")])
|
|
if path:
|
|
archivo_ruta["path"] = path
|
|
lbl.config(text=os.path.basename(path))
|
|
|
|
def ejecutar():
|
|
if not archivo_ruta["path"]: return
|
|
res = procesar_feeds(archivo_ruta["path"], combo.get().lower())
|
|
messagebox.showinfo("Éxito", f"Generado: {res}")
|
|
root.destroy()
|
|
|
|
ttk.Label(root, text="Convertidor OPML/XML", font=("Arial", 12, "bold")).pack(pady=15)
|
|
ttk.Button(root, text="Seleccionar Archivo", command=seleccionar).pack()
|
|
lbl = ttk.Label(root, text="Ninguno", foreground="gray")
|
|
lbl.pack(pady=5)
|
|
combo = ttk.Combobox(root, values=["HTML", "Markdown", "JSON"], state="readonly")
|
|
combo.set("HTML")
|
|
combo.pack(pady=10)
|
|
ttk.Button(root, text="Convertir", command=ejecutar).pack(pady=10)
|
|
root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 1:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("entrada")
|
|
parser.add_argument("-f", "--formato", choices=['html', 'markdown', 'json'], default='html')
|
|
args = parser.parse_args()
|
|
if os.path.exists(args.entrada):
|
|
print(f"Generado: {procesar_feeds(args.entrada, args.formato)}")
|
|
else:
|
|
iniciar_gui()
|