Inicio Apuntes FPApuntes DAMAcceso a Datos Acortador de URLs. Crea URL’s cortas desde el escritorio

Acortador de URLs. Crea URL’s cortas desde el escritorio

Utilizando los servicios gratuitos de pyshorteners

Publicado por entreunosyceros

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)

Programa funcionando en el escritorio de Ubuntu 24.04

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.

Acortador URLs con tema claro

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

  1. 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
  1. El siguiente paso es proceder a la instalación del paquete descargado:
sudo dpkg -i acortadorURLs.deb
  1. Tras la instalación ya tiene que poder ejecutar el programa buscando el lanzador en tu equipo.
Lanzador AcortadorURLs

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í.

También te puede interesar ...

Deja un comentario

* Al utilizar este formulario, aceptas que este sitio web almacene y maneje tus datos.

Adblock Detectado!!

Ayúdanos deshabilitando la extensión AdBlocker de tu navegador para visitar esta web.
Si no sabes hacerlo en Chrome, consulta el siguiente enlace. Si utilizas Firefox, puedes consultar este otro enlace.
Esto mejorará tu experiencia en este sitio web.