Tabla de contenido
Una vez más aquí (que ya hacía como un mes y pido que no publicaba nada en este sitio). Hoy vengo a dejar un pequeño artículo sobre algo que ya hice el año pasado, pero que a día de hoy por una cosa u otra, me resultaba incomodo. Se trata de un acortador de URLs. En aquel artículo el programa funcionaba vía web, utilizando JavaScript, PHP y HTML. Pero a día de hoy esto se me hace más cómodo poder acortar las URL’s desde el escritorio. Y por esta razón, y aprovechando que estoy intentando aprender a utilizar Flet, pues me propuse hacer una aplicación de escritorio que me permita realizar esta tarea utilizando Flet y Python.
Tengo que aclarar, que realmente las URL’s se van a acortar utilizando un paquete ya disponible de Python, por lo que en este caso no vamos a crear directamente las URL cortas como hacíamos en el otro proyecto. En este caso más bien vamos a crear un cliente que permite realizar peticiones a diferentes servicios gratuitos (y que no requieren tokens ni registros) y que nos va a presentar los resultados en nuestro escritorio.
Hoy en día, supongo que es conocido por todo el mundo, o por la mayoría, que acortar URLs se ha convertido en una necesidad para optimizar el uso de enlaces en redes sociales, correos electrónicos y sitios web. En este artículo, exploraremos cómo crear un acortador de URLs utilizando Python y la biblioteca Flet, que por lo que voy viendo, permite construir aplicaciones de escritorio de manera sencilla y eficiente.
¿Qué es un acortador de URLs?
Antes de nada, y por si alguien no lo tiene claro, decir que un acortador de URLs es una herramienta que transforma un enlace largo en uno más corto y manejable (¡aun que no más fácil de recordar!). Esto no solo facilita compartir enlaces, sino que también puede proporcionar análisis sobre el uso de esos enlaces. Aun que el análisis de estos enlaces, se escapa a lo que se busca en este artículo.
Código del proyecto
Este proyecto, tendría que haberlo modularizado un poco más, pero por una cosa u otra, lo he dejado solo en tres archivos básicos. Pero como digo, esta no sería la forma más óptima de realizar un proyecto (pero para lo que yo necesito y para el tiempo que tengo, tendrá que servir)

1. run_app.py
Este archivo va a ser el responsable de crear un entorno virtual, instalar las dependencias necesarias y ejecutar la aplicación principal. A continuación, aquí queda el código del archivo. Que como se puede ver va comentado, por lo que creo que se puede seguir de forma fácil.
import os import subprocess import sys # Obtener el directorio donde se encuentra el archivo run_app.py DIRECTORIO_SCRIPT = os.path.dirname(os.path.abspath(__file__)) # Nombre del entorno virtual VENV_DIR = os.path.join(DIRECTORIO_SCRIPT, "venv") # Crear la ruta completa # Determinar el ejecutable de Python dentro del entorno virtual def obtener_python_ejecutable(): if os.name == 'nt': # Windows return os.path.join(VENV_DIR, "Scripts", "python.exe") else: # Linux/macOS return os.path.join(VENV_DIR, "bin", "python") # Comprobar si el entorno virtual está creado def entorno_virtual_existe(): return os.path.isdir(VENV_DIR) # Crear el entorno virtual def crear_entorno_virtual(): print("Creando el entorno virtual...") subprocess.run([sys.executable, "-m", "virtualenv", VENV_DIR], check=True) # Instalar pip si no está instalado def asegurar_pip(python_executable): try: subprocess.run([python_executable, "-m", "pip", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except subprocess.CalledProcessError: print("pip no está instalado. Intentando instalar pip manualmente...") try: subprocess.run([sys.executable, "-c", "import urllib.request; urllib.request.urlretrieve('https://bootstrap.pypa.io/get-pip.py', 'get-pip.py')"], check=True) subprocess.run([python_executable, "get-pip.py"], check=True) except subprocess.CalledProcessError as e: print(f"Error al instalar pip manualmente: {e}") raise # Instalar las dependencias desde requirements.txt def instalar_dependencias(python_executable): print("Instalando dependencias...") asegurar_pip(python_executable) ruta_requirements = os.path.join(DIRECTORIO_SCRIPT, "requirements.txt") if os.path.exists(ruta_requirements): subprocess.run([python_executable, "-m", "pip", "install", "-r", ruta_requirements], check=True) else: print("No se encontró el archivo requirements.txt.") # Comprobar si flet está instalado en el entorno virtual def flet_instalado(python_executable): try: result = subprocess.run( [python_executable, "-m", "pip", "show", "flet"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) return "Name: flet" in result.stdout except FileNotFoundError: print(f"Archivo no encontrado: {python_executable}") return False except subprocess.CalledProcessError: return False # Mostrar mensajes usando Flet si está instalado def mostrar_mensaje(mensaje): python_executable = obtener_python_ejecutable() if flet_instalado(python_executable): try: import flet as ft def main(page: ft.Page): page.add(ft.Text(mensaje)) page.update() ft.app(target=main) except ImportError: print("Error al importar Flet.") else: print(mensaje) # Ejecutar el script principal dentro del entorno virtual def ejecutar_app(): python_executable = obtener_python_ejecutable() ruta_main = os.path.join(DIRECTORIO_SCRIPT, "main.py") if os.path.isfile(python_executable): subprocess.run([python_executable, ruta_main], check=True) else: print(f"El ejecutable no se encuentra: {python_executable}") # Comprobar si el paquete python3-virtualenv está instalado def verificar_virtualenv_instalado(): try: subprocess.run(["dpkg", "-s", "python3-virtualenv"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return True except subprocess.CalledProcessError: return False # Lógica principal def main(): python_executable = obtener_python_ejecutable() if not entorno_virtual_existe(): print("El entorno virtual no existe. Creando uno nuevo...") if not verificar_virtualenv_instalado(): print("El paquete python3-virtualenv no está instalado. Por favor, instálalo y vuelve a intentarlo.") sys.exit(1) crear_entorno_virtual() instalar_dependencias(python_executable) else: print("El entorno virtual ya existe.") instalar_dependencias(python_executable) if not flet_instalado(python_executable): print("Flet no está instalado. Instalando Flet...") subprocess.run([python_executable, "-m", "pip", "install", "flet"], check=True) print("Flet instalado correctamente.") ejecutar_app() if __name__ == "__main__": main()
main.py
Este archivo contiene la lógica del acortador de URLs. A este archivo se le llamada desde run_app.py, y será el encargador de cargar y mostrar la ventana desde la que el usuario podrá escribir un enlace largo y recibir un listado de enlaces cortos. Estos enlaces vienen provistos desde la librería pyshorteners y los servicios gratuitos con los que nos permite trabajar.
El código de este archivo es el siguiente:
import flet as ft import pystray from pystray import MenuItem as Item, Icon from PIL import Image import asyncio import threading import pyshorteners import os shortener = pyshorteners.Shortener() # Obtén el directorio del archivo actual BASE_DIR = os.path.dirname(os.path.abspath(__file__)) class ShortLinkRow(ft.Row): def __init__(self, shortened_link, link_source): super().__init__() self.tooltip = link_source self.alignment = "center" self.controls = [ ft.Text(value=shortened_link, size=18, selectable=True, italic=True), ft.IconButton( icon=ft.icons.COPY, on_click=lambda e: self.copy(shortened_link), bgcolor=ft.colors.PURPLE_700, tooltip="Copiar" ), ft.IconButton( icon=ft.icons.OPEN_IN_BROWSER_OUTLINED, tooltip="Abrir en el navegador web", on_click=lambda e: e.page.launch_url(shortened_link) ) ] def copy(self, value): self.page.set_clipboard(value) snack_bar = ft.SnackBar(ft.Text("Link copiado al portapapeles!"), open=True) self.page.overlay.append(snack_bar) # Añadir el SnackBar self.page.update() # Actualizar para que se muestre el SnackBar async def hide_splash(page, splash_image): await asyncio.sleep(3) # Esperar 3 segundos splash_image.visible = False page.update() def on_quit(page): page.window.close() def create_tray_icon(page): def quit_action(icon, item): icon.stop() on_quit(page) image = Image.open(os.path.join(BASE_DIR, "assets/icons/loading-animation.png")) # icono bandeja del sistema icon = pystray.Icon("URL Shortener", image, menu=pystray.Menu( Item('Mostrar ventana', lambda: None), Item('Salir', lambda icon, item: quit_action(icon, item)) )) def run_tray(): icon.run() tray_thread = threading.Thread(target=run_tray, daemon=True) tray_thread.start() async def main(page: ft.Page): page.title = "Acortador de URL" page.theme_mode = "dark" page.horizontal_alignment = "center" page.window.width = 540 page.window.height = 640 page.scroll = "hidden" # Hacer que la ventana no sea redimensionable #page.window.resizable = False # Splash image (loading) splash_image = ft.Image( src=os.path.join(BASE_DIR, "assets/icons/loading-animation.png"), width=page.window.width, height=page.window.height, visible=True ) page.overlay.append(splash_image) page.update() await hide_splash(page, splash_image) page.fonts = { "sf-simple": "/fonts/San-Francisco/SFUIDisplay-Light.ttf", "sf-bold": "/fonts/San-Francisco/SFUIDisplay-Bold.ttf" } page.theme = ft.Theme(font_family="sf-simple") def mostrar_snackbar(mensaje): snack_bar = ft.SnackBar(ft.Text(mensaje), open=True) page.overlay.append(snack_bar) page.update() def change_theme(e): page.theme_mode = "light" if page.theme_mode == "dark" else "dark" theme_icon_button.selected = not theme_icon_button.selected page.update() def shorten(e: ft.ControlEvent): user_link = text_field.value if user_link: page.add(ft.Text(f"URL Larga: {text_field.value}", italic=False, weight=ft.FontWeight.BOLD)) try: page.add(ShortLinkRow(shortener.tinyurl.short(text_field.value), "Source: tinyurl.com")) page.add(ShortLinkRow(shortener.osdb.short(text_field.value), "Source: osdb.link")) page.add(ShortLinkRow(shortener.clckru.short(text_field.value), "Source: clck.ru")) page.add(ShortLinkRow(shortener.dagd.short(text_field.value), "Source: da.dg")) page.add(ShortLinkRow(shortener.isgd.short(text_field.value), "Source: is.gd")) mostrar_snackbar("URL acortada con éxito!") except Exception as exception: print(exception) mostrar_snackbar(f"Ocurrió un error: {str(exception)}") page.update() close_dialog = ft.AlertDialog( modal=True, title=ft.Text("¿Estás seguro de que quieres salir?"), content=ft.Text("Cualquier progreso no guardado se perderá."), actions=[ ft.TextButton("Sí", on_click=lambda e: page.window.destroy()), # Cerrar la aplicación ft.TextButton("No", on_click=lambda e: page.close(close_dialog)), # Cerrar el diálogo ], actions_alignment=ft.MainAxisAlignment.END, ) # Diálogo Acerca de def show_about_dialog(e): about_dialog = ft.AlertDialog( modal=True, title=ft.Text("Acerca de", color=ft.colors.WHITE10), content=ft.Container( content=ft.Column( [ ft.Image(src=os.path.join(BASE_DIR, "assets/icons/logo.png"), width=100, height=100), ft.Text( "Esta es una pequeña aplicación\n que permite acortar URL's largas\n a URL's cortas utilizando\n servicios gratuitos que hay en Internet.", text_align=ft.TextAlign.CENTER, color=ft.colors.WHITE ), ], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=10 ), padding=ft.padding.all(10), # Padding ajustado correctamente width=300, # Ancho fijo para el diálogo height=250, # Altura máxima controlada alignment=ft.alignment.center # Centrar contenido dentro del contenedor ), actions=[ ft.TextButton("Cerrar", on_click=lambda e: page.close(about_dialog)) ], actions_alignment=ft.MainAxisAlignment.END, bgcolor=ft.colors.BLACK, inset_padding=ft.Padding(20, 20, 20, 20), # Padding externo ajustado ) page.overlay.append(about_dialog) about_dialog.open = True page.update() # Mostrar el cuadro de diálogo cuando se selecciona "Salir" del menú def confirm_exit(e): page.overlay.append(close_dialog) # Asigna el diálogo a la página close_dialog.open = True # Abre el diálogo page.update() # Cambiar tema theme_icon_button = ft.IconButton( ft.icons.DARK_MODE, selected=False, selected_icon=ft.icons.LIGHT_MODE, icon_size=35, tooltip="Cambiar tema", on_click=change_theme, style=ft.ButtonStyle(color={"": ft.colors.BLACK, "seleccionado": ft.colors.WHITE}), ) # Crear el menú menu = ft.PopupMenuButton( items=[ ft.PopupMenuItem(text="Acerca de", on_click=show_about_dialog), ft.PopupMenuItem(text="Ver código fuente", on_click=lambda e: page.launch_url("https://github.com/sapoclay/acortador-URL-flet")), ft.PopupMenuItem(text="Salir", on_click=confirm_exit), ] ) # Asignar el appbar con el menú page.appbar = ft.AppBar( title=ft.Text("Acortador URL", color="white"), center_title=True, bgcolor="purple", actions=[theme_icon_button, menu], ) # El campo de texto para la URL y el texto explicativo page.add( text_field := ft.TextField( value='https://github.com/sapoclay', label="URL Larga", hint_text="Escribe la URL larga aquí", max_length=200, width=800, keyboard_type=ft.KeyboardType.URL, suffix=ft.FilledButton("Acortar!", on_click=shorten), on_submit=shorten ), ft.Text("URL's generadas:", weight=ft.FontWeight.BOLD, size=23, font_family="sf-bold") ) # Crear el icono en la bandeja del sistema create_tray_icon(page) # Manejar el evento de cierre de la ventana def handle_window_event(e): if e.data == "close": page.open(close_dialog) # Abrir el diálogo de confirmación page.window.prevent_close = True # Evitar el cierre directo page.window.on_event = handle_window_event # Asignar el manejador al evento de ventana page.update() # Ejecutar la aplicación ft.app(target=main, view=ft.AppView.FLET_APP, assets_dir='assets')
Este código no está tan comentado como el anterior, pero creo que si te paras a leerlo se puede ver de forma fácil cómo funciona.
requirements.txt
Este archivo contiene las dependencias necesarias para ejecutar la aplicación. Este archivo será utilizado por el archivo run_app.py para realizar la instalación de los paquetes aquí indicados (en caso de ser necesario).
flet pyshorteners pystray Pillow
assets
Dentro de esta carpeta será necesario incluir las fuentes y los iconos que maneja el programa. Las fuentes están dentro de su correspondiente carpeta, y los iconos tienen también su carpeta correspondientes. Si a alguien le interesa esto, podrá descargar los que yo utilicé para el programa desde el repositorio en GitHub que indicaré al final del artículo.
Cómo ejecutar o instalar la aplicación
Tengo que decir que esta aplicación yo solo la he probado en Ubuntu 24.04. No he tenido tiempo de probarla en Windows. Más que nada por que mi sistema operativo principal, desde donde necesito tener disponible algo como esto es Ubuntu.
Esta aplicación ha sido desarollado utilizando Python3.10, pero con cualquier versión de Python3 debería poder trabajar sin problemas.

Para ejercutar esta aplicación clona su repositorio:
git clone https://github.com/sapoclay/acortador-URL-flet
Dirígente a la carpeta que se acaba de crear:
cd acortador-URL-flet
Ejecuta el script run_app.py
, que automáticamente verificará la existencia de un entorno virtual y las dependencias. Si no existen, las instalará antes de lanzar la aplicación.
python3 run_app.py
Cuando lo lances por primera vez, tardará un poco en iniciarse, ya que la primera vez se va a crear el entorno virtual e instalar las dependencias necesarias.
Instalar como paquete .DEB el acortador de URLs
- Descarga el paquete .deb abriendo una terminal (Ctrl+Alt+T) y ejecuta:
wget https://github.com/sapoclay/acortador-URL-flet/releases/download/0.5/acortadorURLs.deb
- El siguiente paso es proceder a la instalación del paquete descargado:
sudo dpkg -i acortadorURLs.deb
- Tras la instalación ya tiene que poder ejecutar el programa buscando el lanzador en tu equipo.

Repositorio en GitHub del acortador de URLs
Si te interesa disponer de todo el código fuente, de las imágenes y las fuentes, puedes dirigirte al repositorio en Github en el que he alojado el proyecto y descargarlo todo desde ahí.