Created
December 22, 2025 16:17
-
-
Save vbresan/a295a1b75dd58e65f1146a7e9f49ba40 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Minimal Nautilus extension to convert dropped URL shortcuts. | |
| Logs to /tmp/nautilus-drop-test.log | |
| Installation: | |
| 1. Install nautilus-python: sudo apt install python3-nautilus | |
| 2. Copy this file to: ~/.local/share/nautilus-python/extensions/shortcut_converter.py | |
| 3. Restart Nautilus: nautilus -q | |
| 4. Optionally watch the log: tail -f /tmp/nautilus-shortcut-converter.log | |
| Then try dragging a link from Firefox into a folder. | |
| """ | |
| import gi | |
| gi.require_version("Gtk", "4.0") | |
| from gi.repository import Nautilus, GObject | |
| from urllib.parse import unquote | |
| import datetime | |
| import os | |
| import re | |
| import urllib.request | |
| LOG_FILE = "/tmp/nautilus-shortcut-converter.log" | |
| def log(message: str) -> None: | |
| """Write a message to the log file""" | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| with open(LOG_FILE, "a") as f: | |
| f.write(f"[{timestamp}] {message}\n") | |
| def get_filepath(uri: str) -> str: | |
| """Decode the URI to get the actual file path""" | |
| try: | |
| return unquote(uri.replace("file://", "")) | |
| except: | |
| return uri.replace("file://", "") | |
| def is_link(filename: str) -> bool: | |
| """Check if the filename indicates a dropped link""" | |
| return filename.startswith(("http:", "https:")) and filename.endswith(".txt") | |
| def get_website_title(url: str, timeout: int = 5) -> str: | |
| """Fetch the title of a website from its URL""" | |
| try: | |
| # Set a user agent to avoid being blocked | |
| req = urllib.request.Request( | |
| url, | |
| headers={"User-Agent": "Mozilla/5.0"} | |
| ) | |
| # Fetch the page | |
| with urllib.request.urlopen(req, timeout=timeout) as response: | |
| html = response.read().decode("utf-8", errors="ignore") | |
| # Extract title using regex | |
| match = re.search(r"<title[^>]*>(.*?)</title>", html, re.IGNORECASE | re.DOTALL) | |
| if match: | |
| title = match.group(1).strip() | |
| title = re.sub(r"\s+", " ", title) | |
| return title | |
| return None | |
| except Exception as e: | |
| return None | |
| def remove_head(filename: str) -> str: | |
| """Remove leading http:-- or https:-- from the filename""" | |
| if filename.startswith("https:--"): | |
| return filename[8:] | |
| elif filename.startswith("http:--"): | |
| return filename[7:] | |
| else: | |
| return filename | |
| def get_windows_filename(filename: str) -> str: | |
| """Convert filename to Windows-compatible format""" | |
| name, ext = os.path.splitext(filename) | |
| name = re.sub(r"[<>:\"/\\|?*]", "-", name) | |
| name = re.sub("-+", "-", name) | |
| if len(name) > 200: | |
| name = name[:200] | |
| name = name.strip(". -") | |
| return name + ext | |
| def get_unique_filepath(filepath: str, filename: str) -> str: | |
| """Generate a unique filepath by appending (N) if the file exists""" | |
| directory = os.path.dirname(filepath) | |
| new_filepath = os.path.join(directory, filename) | |
| # Avoid overwriting existing files | |
| counter = 2 | |
| while os.path.exists(new_filepath): | |
| name, ext = os.path.splitext(filename) | |
| numbered_filename = f"{name} ({counter}){ext}" | |
| new_filepath = os.path.join(directory, numbered_filename) | |
| counter += 1 | |
| return new_filepath | |
| class ShortcutConverterExtension(GObject.GObject, Nautilus.MenuProvider): | |
| """Nautilus extension to convert dropped browser links (.txt) to Windows .url shortcuts. | |
| Processes files on selection, logs actions, and modifies matching files. | |
| """ | |
| def __init__(self) -> None: | |
| """Initialize the extension and log startup.""" | |
| super().__init__() | |
| log("=" * 60) | |
| log("ShortcutConverterExtension initialized") | |
| def get_file_items(self, files: list[Nautilus.FileInfo]) -> list[Nautilus.MenuItem]: | |
| """Process selected files for link conversion (returns empty list as no menu items are added)""" | |
| for file_info in files: | |
| filepath = get_filepath(file_info.get_uri()) | |
| filename = os.path.basename(filepath) | |
| if not is_link(filename): | |
| continue | |
| log(f"--- NEW LINK DROPPED ---") | |
| log(f"Filename: {filename}") | |
| log(f"Path : {os.path.dirname(filepath)}") | |
| # Try to read and update file content | |
| if os.path.isfile(filepath): | |
| try: | |
| with open(filepath, "r+", encoding="utf-8", errors="ignore") as f: | |
| content = f.read().strip() | |
| new_content = f"[InternetShortcut]\r\nURL={content}\r\n" | |
| f.seek(0) | |
| f.write(new_content) | |
| log(f"Content replaced with Internet Shortcut format") | |
| except Exception as e: | |
| log(f"Error reading file: {e}") | |
| else: | |
| log("(Not a regular file or cannot access)") | |
| # Rename the file | |
| new_filename = get_website_title(content) | |
| if not new_filename: | |
| new_filename = remove_head(filename) | |
| new_filename = get_windows_filename(new_filename) | |
| new_filename = os.path.splitext(new_filename)[0] + ".url" | |
| log(f"New filename: {new_filename}") | |
| new_filepath = get_unique_filepath(filepath, new_filename) | |
| try: | |
| os.rename(filepath, new_filepath) | |
| log(f"Successfully renamed to: {os.path.basename(new_filepath)}") | |
| except Exception as e: | |
| log(f"Error renaming file: {e}") | |
| log("") # Blank line for readability | |
| return [] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment