Obsidian Bookmark Seite, aber schöner

Einige Lesezeichen. Schmuckbild. Mit invokeAI und Stable-Diffusion XL erzeugt

Anmerkung:

In die­sem Artikel beschrei­be ich aus­führ­lich, wie ich einen Alfred Workflow erstel­le. Mein Ziel war es, nicht nur den Workflow selbst zu erstel­len, son­dern auch den Entwicklungsprozess zu doku­men­tie­ren. Bereits in die­sem Artikel habe ich, unten im Nachtrag, eine Verbesserung vor­ge­stellt. Nun, im März 2024, muss­te ich jedoch fest­stel­len, dass wei­te­re Anpassungen nötig sind, die ich im Artikel ‘Python-Skripte in Alfred-Workflows mit vir­tu­el­len Umgebungen nut­zen’ beschrei­be. Dieser Artikel ist trotz­dem eine Voraussetzung, damit der Workflow nach­ge­baut wer­den kann.

Wie in mei­nem letz­ten Beitrag erwähnt, habe ich mich dar­an gemacht, mei­nen Bookmark-Workflow für Obsidian mit einem schö­ne­ren Ausgabeformat zu ver­se­hen. Da ich die weni­gen Workflows, die ich bis­her gebaut habe, mit bash oder zsh erstellt habe, woll­te ich es mal mit Python ver­su­chen.

Auch dies­mal habe ich ChatGPT benutzt. Da ich für ein Bookmark die glei­chen Informationen benö­ti­ge wie für eine Linkpreview, also ein Bild und einen Textauszug der URL, von der aus das Bookmark erstellt wer­den soll, habe ich ChatGPT gebe­ten, ein Python-Skript unter Verwendung der link­pre­view-libra­ry zu erstel­len, das die­se Informationen abfragt und als Markdown aus­gibt. Das Ergebnis war der fol­gen­de Code:

import linkpreview

# Replace 'your_url_here' with the URL you want to generate a preview for
url = 'https://ileif.de'

# Fetch link preview information
preview = linkpreview.generate_preview(url)

# Generate the Markdown link preview tile
markdown_tile = f"""
[![{preview['title']}]({preview['image']})]({url})
**[{preview['title']}]({url})**
{preview['description']}
"""

print(markdown_tile)

Dieser Vorschlag stieß lei­der auf einen Fehler. Als Alternative bot mir ChatGPT einen ande­ren Code mit der Beautiful Soup Library an, anstatt eine Lösung für den Fehler zu prä­sen­tie­ren. So muss­te ich selbst Google bemü­hen, um die rich­ti­ge Syntax für den Aufruf der ent­spre­chen­den Funktionen zu fin­den, die dann auch als Lösung von ChatGPT akzep­tiert und ent­spre­chend eige­baut wur­de:

from linkpreview import link_preview

# Replace 'url' with the actual URL you want to generate a preview for
url = "https://ileif.de"

# Fetch link preview information
preview = link_preview(url, parser="lxml")

# Generate the Markdown link preview tile
markdown_tile = f"""
[![{preview.title}]({preview.image})]({url})
**[{preview.title}]({url})**
{preview.description}
"""

print(markdown_tile)

Und tat­säch­lich nun funk­tio­nier­te das Skript. Als Ergebnis wur­de fol­gen­des aus­ge­ge­ben:

[![dit und dat](None)](https://ileif.de)
**[dit und dat](https://ileif.de)**
In die­sem Blogeintrag beschrei­be ich, wie ich einen Alfred Workflow (inkl. kos­ten­pflich­ti­gem Powerpack) erstellt habe, mit dem ich per Hotkey ein Lesezeichen auf einer dafür vor­ge­se­he­nen Obsidian Notizseite hin­zu­fü­ge. Wobei ich wie­der mit Hilfe von ChatGPT ein Shell-Script für die­sen Workflow erstellt habe. Die Vorgehensweise und auch das Script kön­nen sicher­lich auch für Implementierungen ohne…

In der ers­ten Zeile soll­te eigent­lich ein Link zu einem Bild ste­hen, da die Homepage aber kein Bild hat, steht hier „None”. Danach folgt eine Leerzeile und der Titel der Website inkl. Link und in der nächs­ten Zeile ein klei­ner Textauszug.

Bei einem Beitrag mit einem ent­spre­chen­den Bild sieht das Ergebnis dann so aus:

[![NFC Tags nutzen - dit und dat](https://ileif.de/wp-content/uploads/2023/08/IMG_0215-472x1024.png)](https://ileif.de/2023/08/16/nfc-tags-nutzen/)**[NFC Tags nutzen - dit und dat](https://ileif.de/2023/08/16/nfc-tags-nutzen/)**
Auf iPhones kön­nen NFC-Tags rela­tiv ein­fach für ver­schie­de­ne Aufgaben ein­ge­setzt wer­den. Dabei gibt es zwei Möglichkeiten. Zum einen der Apple-Weg, bei dem das Erkennen eines Tags eine Aktion oder einen Kurzbefehl aus­löst. Der zwei­te Weg ist das Beschreiben des Tags mit Daten, wel­che die Aktion defi­nie­ren, die beim Erkennen des Tags aus­ge­löst wer­den soll. was auch bei Android funk­tio­niert.

Da ich aber immer ein Bild ange­zeigt haben möch­te, ist noch eine Abfrage not­wen­dig, die einen Fallback mit einem „No Image” Bild ein­baut, wenn die Abfrage nach dem Bild „None” ergibt. Außerdem muss der Code so erwei­tert wer­den, dass beim Aufruf die URL als Parameter über­ge­ben wer­den kann:

import sys
from linkpreview import link_preview

def generate_markdown_tile(url):
    preview = link_preview(url, parser="lxml")
    
    # Check if the image is None and replace it with a default image URL
    if preview.image is None:
        preview.image = "https://example.com/noimage.png"
    
    markdown_tile = f"""
[![{preview.title}]({preview.image})]({url})
**[{preview.title}]({url})**
{preview.description}
"""
    return markdown_tile

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script_name.py <url>")
        sys.exit(1)
    
    url = sys.argv[1]
    markdown_tile = generate_markdown_tile(url)
    print(markdown_tile)

An die­ser Stelle ist es wich­tig zu erwäh­nen, dass der Variablen markdown_tile ein for­ma­tier­ter mehr­zei­li­ger String zuge­wie­sen wird, wes­halb bei der Zuweisung die Syntax f""" bla bla bla """ ver­wen­det wird.

Da ich nun mit dem Python-Skript zufrie­den war, frag­te ich ChatGPT nach einer iden­ti­schen Lösung als zsh-Skript, was aber damit ende­te, dass auch dafür link­pre­view zunächst als Bibliothek und dann als Programm vor­ge­schla­gen wur­de, was bei­des nicht zu exis­tie­ren scheint. Am Ende schlug mir ChatGPT noch eine Lösung vor, bei der der Python-Code kom­plex inner­halb des Scripts auf­ge­ru­fen wur­de.

Deshalb habe ich mich ent­schie­den, bei dem Python Skipt zu blei­ben und die­ses nun von ChatGPT erwei­tern zu las­sen, so dass bei einem Aufruf in einer Markdown-Datei nach der unsicht­ba­ren Zeile %%insert%% (sie­he auch den vor­he­ri­gen Beitrag) das Ergebnis ein­ge­fügt wird.

Die vor­ge­schla­ge­nen Lösungen konn­te ich nicht ver­wen­den, da ChatGPT nicht damit umge­hen konn­te, dass das Ergebnis des Skripts ein for­ma­tier­ter mehr­zei­li­ger String war und die vor­ge­schla­ge­nen Aufrufe mit sed oder awk ein­fach nicht damit umge­hen konn­ten. Nachdem mir dann auch noch ein Perl-Aufruf als Lösung vor­ge­schla­gen wur­de, muss­te ich wie­der selbst „krea­tiv” wer­den und Google bemü­hen. Als Lösung habe ich mich dann ent­schie­den, das Markdown in einer tem­po­rä­ren Datei zu spei­chern und die­se an der ent­spre­chen­den Stelle in die Bookmark-Datei ein­zu­fü­gen. Dazu habe ich einen ent­spre­chen­den sed Befehl gefun­den, der auch funk­tio­niert hat.

Um es kurz zu machen, der end­gül­ti­ge Code, wie er jetzt im Alfred Workflow ver­wen­det wird, sieht wie folgt aus:

#!/opt/homebrew/bin/python3
import sys
from linkpreview import link_preview

def generate_markdown_tile(url):
    preview = link_preview(url, parser="lxml")
    
    # Check if the image is None and replace it with a default image URL
    if preview.image is None:
        preview.image = "https://example.com/noimage.png"
    
    markdown_tile = f"""
<table padding=50px>
    <tr>
        <td> 
            <img src="{preview.image}" alt="{preview.title}" width="200" style="padding: 10px;"> 
        </td> 
        <td> 
            <a href="{url}" style="font-size: 16px;">{preview.title}</a>
            <br>
            <p>{preview.description}</p>
        </td>
    </tr> 
</table>
"""
    return markdown_tile

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script_name.py <url>")
        sys.exit(1)
    
    url = sys.argv[1]
    markdown_tile = generate_markdown_tile(url)
    print(markdown_tile)

Ich habe auch die Formatierung des Eintrags geän­dert. Da Markdown kei­ne ver­schach­tel­ten Tabellen inter­pre­tie­ren kann, habe ich auf HTML umge­stellt, das auch in Obsidian pro­blem­los ger­en­dert wird. Den Shebang muss­te ich ein­fü­gen, weil ich mit Python in Alfred Probleme hat­te (s.u.).

Im Prinzip ist der Ablauf ähn­lich wie im vor­he­ri­gen Artikel: Zuerst wird der akti­ve Browser abge­fragt, dann wird auf­grund der Funktionsweise der Alfred-Module kurz nach Chrome und Safari ein sepa­ra­ter Weg ein­ge­schla­gen, um die URL des akti­ven Browserfensters abzu­fra­gen, die dann als Input für das obi­ge Skript ver­wen­det wird. Das Ergebnis wird dann an das fol­gen­de zsh-Skript über­ge­ben, das den Eintrag in die ent­spre­chen­de Datei in mei­nem Obsidian Vault schreibt:

# Replace with the actual variable content
markdown_callout="{query}"

# Source file containing content to insert
temp_md=$(mktemp)
echo "$markdown_callout" > "$temp_md"

# Destination file where content will be inserted
destination_file="/PathToMyVault/01 Documents/Bookmarks.md"

# Special string indicating the position to insert
insert_marker="%%ins%%"

# Create a temporary file to store modified content
temp_file=$(mktemp)

# Insert the content of temp_md into destination_file using sed
sed "/${insert_marker}/r ${temp_md}" "${destination_file}" > "${temp_file}"

# Replace the original destination_file with the modified content
mv "${temp_file}" "${destination_file}"

# Clean up temporary files
rm "$temp_md"

echo "Done"

Wahrscheinlich hät­te ich die­sen Teil auch im Python-Script machen kön­nen, aber da ich Probleme mit der Verwendung von Python in Alfred hat­te, habe ich es bei die­ser Lösung belas­sen (never chan­ge a run­ning sys­tem). Das Problem ist ein­fach, dass wenn man Python als Sprache in der Scriptbox aus­wählt, immer das auf dem Mac instal­lier­te Python unter /usr/bin/python3 auf­ge­ru­fen wird, das aber irgend­wie auf Fehler läuft. Also habe ich das Script als exter­nes Script ein­ge­bun­den und mit dem she­bang noch sicher­ge­stellt, dass auch das neue­re von brew instal­lier­te Python unter /opt/homebrew/bin/python3 ver­wen­det wird. Am ein­fachs­ten erstellt man ein exter­nes Skript in der Skriptbox, wo man ein­fach einen Namen ein­gibt, dann wird die­se Datei im Workflow-Ordner erstellt, wenn sie noch nicht exis­tiert und über den Button „Skript öff­nen” wird die­se dann im bevor­zug­ten Editor geöff­net und der Code kann hin­ein kopiert wer­den.

Jetzt wer­den mei­ne Lesezeichen auf mei­ner Bookmarkseite in Obsidian etwas schö­ner ange­zeigt.

Leider gibt es zwei Probleme mit die­ser Methode. Einige Websites schei­nen es nicht zu mögen, wenn sie mit link­pre­view auf­ge­ru­fen wer­den. So kann ich mit die­sem Skript z.B. die Seite https://www.cyberciti.biz nicht book­mar­ken. Ein zwei­tes Problem ist in der Abbildung oben zu sehen. Meine WordPress Installation wei­gert sich, eine Zusammenfassung für neue­re Artikel zu erstel­len. Vielleicht ist das ein Fehler in der letz­ten Version von WordPress. In mei­nen RSS-Feed taucht der Text auf. Im Nachtrag stel­le ich einen Workaround vor.

Nachtrag:

Das Problem mit den Websites, die sich wei­gern, ihre Geheimnisse preis­zu­ge­ben, habe ich noch nicht gelöst, wahr­schein­lich muss man eine Bibliothek ver­wen­den, die den Browser-Agenten vor­täuscht — da muss ich noch recher­chie­ren. Aber ich habe einen Workaround für mein WordPress-Problem gefun­den. Ich benut­ze nun doch Beautiful Soup, zumin­dest für den Teil, der einen Text als Zusammenfassung auf­be­rei­tet. Dazu habe ich das Skript von oben so erwei­tert, dass Beautiful Soup den ers­ten Absatz des Textes als Zusammenfassung extra­hiert, falls link­pre­view kei­nen Text über­ge­ben bekommt. Ich habe zwar auch ver­sucht, das gan­ze Skript mit Beautiful Soap zu rea­li­sie­ren, aber lei­der hat link­pre­view das „rich­ti­ge­re” Bild gefun­den und Beautiful Soups Bilder-Bild fand ich nicht gut. Also nut­ze ich erst­ein­mal bei­de Bibliotheken.

#!/opt/homebrew/bin/python3

import sys
from linkpreview import link_preview
import requests
from bs4 import BeautifulSoup

def get_first_paragraph(soup):
    paragraph = soup.find('p')
    if paragraph:
        return paragraph.get_text().strip()
    return "No excerpt available"

def generate_markdown_tile(url):
    preview = link_preview(url, parser="lxml")
    
    # Check if the image is None and replace it with a default image URL
    if preview.image is None:
        preview.image = "https://example.com/noimage.png"
    
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    
    if not preview.description:
        description = get_first_paragraph(soup)
    else:
        description = preview.description
    
    markdown_tile = f"""
<table padding=50px>
    <tr>
        <td> 
            <img src="{preview.image}" alt="{preview.title}" width="200" style="padding: 10px;"> 
        </td> 
        <td> 
            <a href="{url}" style="font-size: 16px;">{preview.title}</a>
            <br>
            <p>{description}</p>
        </td>
    </tr> 
</table>
"""
    return markdown_tile

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python script_name.py <url>")
        sys.exit(1)
    
    url = sys.argv[1]
    markdown_tile = generate_markdown_tile(url)
    print(markdown_tile)

Und so wird dann nun auch der feh­len­de Text ange­zeigt.

Das ist zwar ein wil­der Hack, aber viel­leicht fin­de ich ja auch noch eine bes­se­re Lösung. Erstmal reicht es aber für mich voll aus.

Nachtrag 2:

Einen Tag nach der Veröffentlichung die­ses Beitrages, wur­de ich im Alfred Blog auf eine neu­en Automative Task Gruppe auf­merk­sam gemacht, die einer­seits den Alfred Workflow ver­ein­facht und auch erwei­tert, aber auch einen Nachteil hat.

Anstatt zuerst abzu­fra­gen, in wel­chem Browser der Hotkey gedruckt wur­de, um die unter­schied­li­chen Routinen für Chrome- und Webkit-basier­te Browser auf­zu­ru­fen, gibt es nun einen neu­en Automation Task „Get Current Frontmost Browser Tab”, der dies intern ver­ar­bei­tet und die URL und/oder den Titel als Ausgabe lie­fert.

So kann ich den Workflow ver­ein­fa­chen und er funk­tio­niert nun ohne gro­ßen Aufwand in allen Browsern. Ein klei­ner Nachteil ist, dass ich nicht abfra­gen kann, ob die App „front­most” kein Browser ist, um einen Hinweis zu sen­den, dass der Workflow an die­ser Stelle nicht funk­tio­niert.

Das war’s. Wie immer, Verbesserungsvorschläge, Fragen oder Sonstiges bit­te in den Kommentaren hin­ter­las­sen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert