Vom Weltraum direkt auf den Desktop: So funktioniert’s!

Wenn du ein großer Fan der Weltraumfotografie bist, dann ist dieser Beitrag genau das Richtige für dich. In diesem Leitfaden zeige ich dir, wie du das Astronomie-Bild des Tages über die NASA-API herunterlädst und damit deinen Desktop-Hintergrund immer wieder aufs Neue ändern kannst. Und das Beste daran ist, dass du nur geringe Kenntnisse über das Scripting mit Python haben musst.

Gesagt sei aber, dass diese Anleitung für Ubuntu als Betriebssystem ausgelegt ist. Selbstredend, kann der Artikel aber auch als Grundlage für ein MAC- oder Windows-Skript dienen. So oder so habe ich eine möglichst leicht verständliche Anleitung samt umfangreichen Code verfasst. Sei also gespannt, wie du deinen Desktop mit den atemberaubenden Bildern der NASA verschönern kannst!

Wie schaut das finale Ergebnis aus?

Fast alle Python-Tutorials im Netz spannen einen viel zu lange auf die Folter, wie der fertige Code aussieht. Oftmals hat man ein paar Minuten mit dem Lesen verbracht, um dann zu merken, dass man auf der Suche nach einem völlig anderen Ansatz war. Daher möchte ich dir direkt zu Beginn das endgültige Ergebnis zeigen. So kannst du direkt abschätzen, ob sich das Lesen für dich lohnt:

#!/usr/bin/env python3

import requests
import os
import logging

# Ordner für Bilder-Downloads und das Log-File:
folder = "/home/fwuest/Downloads/nasa"
logfile = "/home/fwuest/Downloads/nasa/nasa.log"

# Prüfen, ob es den Ordner schon gibt und falls nicht, dann anlegen:
if not os.path.exists(folder):
    if os.makedirs(folder) != 0:
        raise Exception("Der Ordner für die heruntergeladenen Bilder und die Log-Datei konnte nicht erstellt werden.")

# Die Datei für das Logging erstellen:
if not os.path.exists(logfile):
    try:
        with open(logfile, 'w') as f:
            pass
    except:
        raise Exception("Die Datei für die Logging-Meldungen konnte nicht angelegt werden.")

# Konfigurieren des Loggings:
logging.basicConfig(filename=f"{logfile}", level=logging.INFO, format="%(asctime)s:%(levelname)s:%(message)s")

# Prüfe, ob das Modul requests installiert ist und hole das gegebenenfalls nach:
try:
    import requests
except ImportError:
    logging.warning("Das Modul requests muss nachinstalliert werden. Hierfür muss gegebenenfalls das Sudo-Passwort eingegeben werden.")
    if os.system("sudo pip3 install requests") != 0:
        logging.critical("Das Modul requests konnte nicht installiert werden.")
        raise Exception
    import requests

# API anfragen:
API_KEY = "DEMO_KEY"
url = f'https://api.nasa.gov/planetary/apod?api_key={API_KEY}'
response = requests.get(url)

if response.status_code != 200:
    logging.error(f"Die NASA API konnte nicht kontaktiert werden. Fehlercode: {response.status_code}")
    raise Exception

api_daten = response.json()
bild_url = api_daten['hdurl']
bild_name = api_daten['title']

# Bild herunterladen:
response = requests.get(bild_url)

if response.status_code != 200:
    logging.error(f"Das NASA Astronomy Picture of the Day konnte nicht heruntergeladen werden. Fehlercode: {response.status_code}")
    raise Exception

# Bild abspeichern:
bild_pfad = os.path.join(folder, bild_name + ".jpg")
try:
    with open(bild_pfad, "wb") as f:
        f.write(response.content)
except Exception as e:
    logging.error('Das Foto konnte nicht gespeichert werden.')
    raise Exception
    
# Bild als Desktop-Hintergrund nutzen:
if os.system('gsettings set org.gnome.desktop.background picture-uri-dark "file://' + bild_pfad + '"') != 0:
    logging.error("Das NASA Astronomy Picture of the Day konnte nicht als Desktophintergrund hinterlegt werden.")
else:
    logging.info("Das NASA Astronomy Picture of the Day konnte als Desktophintergrund hinterlegt werden.")

Wie funktioniert der Code?

Auf die Wahl der richtigen Module kommt es bei Python an. Um eine Anfrage an die NASA-API richten zu können, wird die Bibliothek requests benötigt. OS hingegen hilft uns beim Anlegen von Ordnern oder dem Ausführen von Linux-Kommandobefehlen. Und bei Logging ist die Bezeichnung Programm. Die enthaltenen Funktionen erstellen die umfangreiche Log-Datei samt Zeitstempel.

Ich selbst bin noch ein großer Fan davon, den Rückgabewert von Shell-Befehlen zu prüfen und gegebenenfalls eine Fehlerbehandlung einzubauen. Das gilt übrigens auch für nativen Python-Code. Daher findest du im Quelltext immer wieder das Try- und Except-Konstrukt. Auf diese Weise kann man als Programmierer sicherstellen, dass das Skript möglichst robust und stabil läuft.

Die Funktionsweise des Codes lässt sich in wenigen Punkten zusammenfassen. Mein kleines Python-Skript lädt das „Astronomy Picture of the Day“ herunter und legt es als neues Hintergrundbild fest. Mithilfe eines Cronjobs kannst du das Skript einmal täglich ausführen lassen und dich am Anblick eines neuen Hintergrundbilds erfreuen. Wie das Skript im Groben funktioniert, steht im Folgenden beschrieben:

  1. Einen Download-Ordner erstellen: Es wird überprüft, ob es das Verzeichnis für den Download der Bilder und das Log-File bereits gibt. Falls nicht, wird der Ordner mithilfe des OS-Moduls angelegt. Eine wirklich praktische Funktion, die nicht nur auf Linux-Systemen funktioniert.
  2. Ein Log-File anlegen: Irgendwo hin muss man die Ereignisse eines Skriptes immer hinschreiben. Daher wird eigens hierfür eine Datei mit der Bezeichnung nasa.log erstellt, falls es diese noch nicht geben sollte. Da es hierfür keine Funktion innerhalb des OS-Moduls gibt, muss ein Umweg über eine sogenannte Kontext-Management-Anweisung gegangen werden.
  3. Das Logging einrichten: Das Logging wird so konfiguriert, dass alle Meldungen ab dem Level INFO gespeichert werden. Darüber hinaus wird ein Format für die Einträge festgelegt. In der Log-Datei findet man dann Einträge wie zum Beispiel: 2023-02-11 08:30:35: INFO: Das NASA Astronomy Picture of the Day konnte als Desktophintergrund hinterlegt werden.
  4. Das Moduls Requests einspielen: Es wird überprüft, ob das Modul Requests installiert ist, und falls nicht, wird es mithilfe des Python Package Managers nachinstalliert. Gebraucht wird es, um mit der API interagieren zu können. Sollte es nicht installiert sein oder schlicht nicht funktionieren, ist die Funktionsweise des Skriptes nicht gegeben.
  5. Die NASA API anfragen: Wie bereits erwähnt, muss eine erfolgreiche Verbindung zur NASA API aufgebaut werden. Ansonsten wird uns der Download des „Astronomy Picture of the Day“ verwehrt. Falls diese Kommunikation nicht erfolgreich ist, wird eine Fehlermeldung im Log-File hinterlegt und das Skript sofort beendet.
  6. Herunterladen des Bildes: Da es sich hierbei um eine High-Definition-Aufnahme handelt, kann dieser Vorgang schon mal ein paar Sekunden dauern. Dateigrößen von über 10 MB sind nämlich keine Seltenheit. Sollte die Bandbreite der eigenen Internetverbindung dafür nicht ausreichen, kann in der JSON-Ausgabe nach dem Key: value URL gesucht werden. Das dort hinterlegte Foto ist deutlich schlanker.
  7. Abspeichern des Fotos: Noch befindet sich das Bild lediglich im flüchtigen Arbeitsspeicher. Für eine persistente Speicherung und spätere Verwendung als Desktop-Hintergrund muss es noch auf der Festplatte abgelegt werden. Auch dieser Schritt benötigt wieder eine Kontext-Management-Anweisung.
  8. Verwenden als Desktop-Hintergrund: Letztendlich wird das „Astronomy Picture of the Day“ als Desktop-Wallpaper aktiviert. Dies gelingt zum Glück mit nur einem einzigen Befehl. Möglich macht es die Desktopumgebung Gnome, die standardmäßig mit Ubuntu ausgeliefert wird. Auch hier führt ein Unix-Rückgabewert größer 0 zu einem Logeintrag.

Was macht jede einzelne Zeile genau?

Nun kommen wir endlich zum spannendsten Teil des Beitrags. Nämlich der Erklärung, wie mein Sourcecode bis ins kleinste Detail funktioniert. Ich versuche dabei so genau wie nur möglich vorzugehen, damit auch absolute Python-Neulinge alles gut nachvollziehen können. Ich erkläre dabei sowohl die grundlegende Funktionsweise der Blöcke als auch meine Herangehensweise.

Die ersten Zeilen meines Skriptes sind noch nicht besonders faszinierend, sollen aber aufgrund ihrer Wichtigkeit nicht vernachlässigt werden:

#!/usr/bin/env python3

import requests
import os
import logging
  • #!/usr/bin/env python3 ist ein sogenannte Shebang, die dem Betriebssystem mitteilt, welches Programm verwendet werden soll, um das Skript auszuführen. In diesem Fall wird das System Python in der Version 3 verwenden, um das Skript auszuführen.
  • import requests ist eine Instruktion, die das Requests-Modul importiert. Requests ist eine sehr nützliche Bibliothek in Python, die es ermöglicht, HTTP/S-Anfragen zu senden und auf die Antworten zu reagieren.
  • import os ist eine Anweisung, die das OS-Modul importiert. OS ist ein Standardmodul in Python, das Funktionen bereitstellt, um mit dem Betriebssystem und den Dateien auf dem Computer zu interagieren.
  • import logging ist ein Befehl, der das Logging-Modul importiert. Logging ist ebenfalls ein Standardmodul in Python, das es ermöglicht, Meldungen über den Fortschritt des Programms und andere wichtige Ereignisse zu protokollieren. Es ist sehr nützlich bei der Fehlerbehebung und Überwachung von Skripten.
folder = "/home/fwuest/Downloads/nasa"
logfile = "/home/fwuest/Downloads/nasa/nasa.log"

Wo die ganzen Bilder abgespeichert werden sollen, kann das Skript weder erraten noch willkürlich festlegen. Hier musst du also die für dich passende Pfade eintragen. Die beiden Variablen werden später noch im Code abgefragt, um den Ordner sowie die Log-Datei anlegen zu können:

if not os.path.exists(folder):
    if os.makedirs(folder) != 0:
        raise Exception("Der Ordner für die heruntergeladenen Bilder und die Log-Datei konnte nicht erstellt werden.")

# Die Datei für das Logging erstellen
if not os.path.exists(logfile):
    try:
        with open(logfile, 'w') as f:
            pass
    except:
        raise Exception("Die Datei für die Logging-Meldungen konnte nicht angelegt werden.")
  • if not os.path.exists(folder): überprüft, ob das Verzeichnis mit dem Variablenwert für folder bereits vorhanden ist. Wenn es nicht vorhanden ist, wird der nachfolgende Code ausgeführt.
  • os.makedirs(folder) ist eine Funktion des OS-Moduls, die ein Verzeichnis mit dem Variablenwert für folder erstellt, inklusive aller notwendigen Unterverzeichnisse. Die Funktion gibt 0 zurück, wenn der Ordner erfolgreich erstellt wurde, und einen anderen Wert, wenn ein Fehler aufgetreten ist.
  • if os.makedirs(folder) != 0: überprüft, ob das Verzeichnis erfolgreich erstellt wurde. Wenn es nicht erfolgreich war, wird eine Exception mit der Nachricht „Der Ordner für die heruntergeladenen Bilder und die Log-Datei konnte nicht erstellt werden.“ ausgelöst.
  • if not os.path.exists(logfile): überprüft, ob bereits eine Log-Datei vorhanden ist. Wenn dem nicht so ist, wird noch etwas Code ausgeführt, um sie anzulegen:
  • with open(logfile, ‚w‘) as f: ist ein Konstrukt in Python, das eine Datei im Schreibmodus öffnet. ‚w‘ bedeutet, dass die Datei überschrieben wird, wenn sie bereits vorhanden ist. Andernfalls wird eine neues File erstellt wird, wenn es noch keine passende Logdatei gibt.
  • pass ist ein Platzhalter, der nichts tut. Hier wird die Datei geöffnet und sofort wieder geschlossen, um sicherzustellen, dass diese vorhanden ist.
  • except: fängt jede Ausnahme ab, die beim Öffnen und Schließen der Datei aufgetreten ist.
  • raise Exception(„Fehlermeldung“) wirft eine Exception mit der Nachricht „Die Datei für die Logging-Meldungen konnte nicht angelegt werden.“ aus, falls ein Fehler beim Öffnen und Schließen der Datei aufgetreten ist.

Weiter geht es in meinem Skript mit dem Einrichten des Loggings:

logging.basicConfig(filename=f"{logfile}", level=logging.INFO, format="%(asctime)s:%(levelname)s:%(message)s")

Dieser Code konfiguriert das Python-Modul Logging, um Protokolle in einer Datei zu speichern. Hinter logging.basicConfig verbirgt sich eine Funktion des Logging-Moduls, die die grundlegenden Einstellungen für das Protokollieren festlegt. Die Argumente sind:

  • filename: Hier wird mit f“{logfile}“ die Datei angegeben, in die das Protokoll geschrieben werden soll. logfile ist eine Variable, die den Namen der Logdatei enthält.
  • level: Hier wird mit logging.INFO das Level des Loggings festgelegt. logging.INFO ist eine Konstante, die angibt, dass nur Informationsmeldungen protokolliert werden sollen.
  • format: Hier wird das Format für die Protokolleinträge festgelegt. „%(asctime)s:%(levelname)s:%(message)s“ gibt an, dass jeder Protokolleintrag aus drei Teilen bestehen soll: einer Zeitangabe (asctime), einer Angabe des Log-Levels (levelname) und einer Nachricht (message), die durch Doppelpunkte voneinander getrennt sind.

Direkt weiter im Quelltext geht es mit einer Fehlerbehandlung:

try:
    import requests
except ImportError:
    logging.warning("Das Modul requests muss nachinstalliert werden. Hierfür muss gegebenenfalls das Sudo-Passwort eingegeben werden.")
    if os.system("sudo pip3 install requests") != 0:
        logging.critical("Das Modul requests konnte nicht installiert werden.")
        raise Exception
    import 

Dieser Code überprüft, ob das Modul requests bereits installiert ist. Falls nicht, wird eine Fehlermeldung generiert und das Modul mittels pip nachinstalliert.

  • try und except bilden einen try-except-Block, der dazu verwendet wird, Fehler in einem bestimmten Bereich abzufangen. In diesem Fall wird im try-Block ein Versuch gestartet, das Modul requests zu importieren. Falls das Modul nicht gefunden wird (ImportError), wird eine Fehlermeldung generiert und das Modul mittels pip nachinstalliert.
  • os.system ist eine Funktion des OS-Moduls, die ein Kommandozeilenbefehl ausführt. In diesem Fall wird „sudo pip3 install requests“ ausgeführt, um das Modul requests zu installieren.
  • Wenn das Nachinstallieren nicht erfolgreich war (os.system liefert nicht 0 zurück), wird eine kritische Fehlermeldung generiert und eine Ausnahme ausgelöst.

Nun sind wir auch schon an einer Stelle im Quelltext angekommen, wo wir zum ersten Mal mit der API interagieren:

API_KEY = "DEMO_KEY"
url = f'https://api.nasa.gov/planetary/apod?api_key={API_KEY}'
response = requests.get(url)

if response.status_code != 200:
    logging.error(f"Die NASA API konnte nicht kontaktiert werden. Fehlercode: {response.status_code}")
    raise Exception

Dieser Code definiert eine URL für den Zugriff auf die NASA Picture of the Day (PoD) API. Die URL enthält den API-Schlüssel API_KEY, der hier auf „DEMO_KEY“ gesetzt ist.

Mit requests.get(url) wird eine HTTP-GET-Anfrage an die angegebene URL gesendet. Das Ergebnis der Anfrage wird in der Variablen response gespeichert.

Anschließend wird überprüft, ob die Anfrage erfolgreich war, indem der HTTP-Statuscode von response geprüft wird. Falls der Statuscode nicht 200 (OK) ist, wird eine Fehlermeldung generiert und eine Ausnahme ausgelöst.

Mit einer Anfrage allein ist es natürlich nicht getan. Die gelieferten Daten müssen natürlich noch verarbeitet werden:

api_daten = response.json()
bild_url = api_daten['hdurl']
bild_name = api_daten['title']

Dieser Code extrahiert Informationen aus dem API-Antwortobjekt, welches sich in response befindet. Zunächst wird das JSON-Format in ein Python-Dictionary umgewandelt, indem die Methode .json() aufgerufen wird. Das Dictionary wird in der Variablen api_daten gespeichert.

Anschließend werden zwei Werte aus dem Dictionary extrahiert:

  • Die URL des heutigen Bilds (PoD) wird unter bild_url gespeichert.
  • Der Titel des heutigen Bilds (PoD) wird unter bild_name gespeichert.

Nun hat man auch schon die URL aus den ganzen Daten gezogen, um mit dem Download des künftigen Desktop-Hintergrunds beginnen zu können:

response = requests.get(bild_url)

if response.status_code != 200:
    logging.error(f"Das NASA Astronomy Picture of the Day konnte nicht heruntergeladen werden. Fehlercode: {response.status_code}")
    raise 

Dieser Code lädt das Foto herunter, welches von der NASA zur Verfügung gestellt wird. Dazu wird die Methode requests.get aufgerufen, welche das Bild aus der URL in der Variablen bild_url herunterlädt. Die Antwort, inklusive des Bildinhalts, wird in einem response-Objekt gespeichert.

Anschließend wird überprüft, ob das Herunterladen des Bilds erfolgreich war. Dies geschieht anhand des HTTP-Statuscodes, der im response-Objekt gespeichert ist. Wenn der Statuscode 200 (OK) ist, wurde das Bild erfolgreich heruntergeladen. Andernfalls wird eine Fehlermeldung im Log-File gespeichert und die Ausführung des Skripts abgebrochen.

Im Optimalfall geht es aber weiter mit dem Abspeichern des Bildes:

bild_pfad = os.path.join(folder, bild_name + ".jpg")
try:
    with open(bild_pfad, "wb") as f:
        f.write(response.content)
except Exception as e:
    logging.error('Das Foto konnte nicht gespeichert werden.')
    raise 

Zuerst wird der Pfad zur zu erstellenden Datei zusammengesetzt. Dazu wird das Verzeichnis (folder) mit dem Dateinamen (bild_name) und der Dateierweiterung („.jpg“) kombiniert. Das Ergebnis wird in der Variablen bild_pfad gespeichert. Anschließend wird ein try-Block gestartet, in dem das heruntergeladene Bild auf der Festplatte gespeichert wird.

Hierfür wird die Funktion open verwendet, welche eine neue Datei erstellt oder eine bestehende öffnet. Die Datei wird im Binärmodus („wb“) geöffnet, damit das Bild als Binärdaten gespeichert werden kann. Die Daten des Bilds befinden sich im response-Objekt und werden in die Datei geschrieben (f.write(response.content)).

Wenn beim Speichern des Bilds eine Ausnahme (Exception) ausgelöst wird, wird eine Fehlermeldung im Log-File gespeichert und das Skript wird abgebrochen. Das wäre aber sehr ärgerlich, da nun nur noch das Hinterlegen des Hintergrundbildes ausgeführt werden muss:

if os.system('gsettings set org.gnome.desktop.background picture-uri-dark "file://' + bild_pfad + '"') != 0:
    logging.error("Das NASA Astronomy Picture of the Day konnte nicht als Desktophintergrund hinterlegt werden.")
else:
    logging.info("Das NASA Astronomy Picture of the Day konnte als Desktophintergrund hinterlegt werden.")

Dieser Code ändert das Hintergrundbild des Gnome-Desktops auf das heruntergeladene NASA Astronomy Picture of the Day. Es wird dazu das Kommandozeilen-Tool ‚gsettings‘ verwendet. Der Befehl ‚gsettings set org.gnome.desktop.background picture-uri-dark „file://‘ + bild_pfad + ‚“‚ setzt das Desktop-Bild auf die im Pfad ‚bild_pfad‘ angegebene Datei.

Wenn das Setzen des Hintergrundbilds erfolgreich war, wird eine Logging-Info-Nachricht geschrieben. Andernfalls wird eine Logging-Error-Nachricht generiert. Zu beachten ist hierbei noch, dass der im Skript verwendete Befehl nur den Dark-Mode anpasst. Solltest du diesen gar nicht verwenden, dann entferne bitte den String -dark.

Was könnte man noch besser machen?

Im Leben sind nur wenige Menschen perfekt und so gibt es immer ein bisschen Verbesserungspotenzial. Zudem wollte ich das Skript bewusst einfach halten, damit jeder Leser meinem Tutorial folgen kann. Allerdings führt so ein Vorgehen zu ein paar Abstrichen in der generellen Qualität. Grundsätzlich sinnvoll wäre es noch, das Skript besser zu strukturieren und auch zu modularisieren.

Dies beinhaltet beispielsweise die Verwendung von Funktionen, um bestimmte Aufgaben zu gruppieren und wiederverwendbar zu machen. Ein weiterer Vorschlag könnte sein, dass das Skript besser dokumentiert wird, um seine Funktionsweise und Absicht klarer zu machen. Es könnte auch hilfreich sein, eine Überprüfung auf mögliche Ausnahmen hinzuzufügen, um unerwartete Fehler zu behandeln.

Zum Beispiel könnten Probleme auftreten, wenn die angegebene Protokolldatei nicht vorhanden ist oder nicht beschreibbar ist. Eine Überprüfung auf diese Ausnahmen kann dazu beitragen, dass das Programm stabiler und zuverlässiger wird. Darüber hinaus könnte es sinnvoll sein, das Skript so zu modifizieren, dass es benutzerdefinierte Konfigurationsoptionen unterstützt.

Dies kann beispielsweise durch die Verwendung von Argumenten auf der Kommandozeile oder durch die Verwendung einer Konfigurationsdatei erreicht werden. Dies sind aber nur einige allgemeine Vorschläge zur Verbesserung des Skripts. Abhängig von den spezifischen Anforderungen könnten andere Änderungen notwendig oder sinnvoll sein.

Noch darauf hinweisen möchte ich an dieser Stelle, dass mich beim Schreiben des Artikels die künstliche Intelligenz ChatGPT tatendurstig unterstützt hat. Wer mehr über das Projekt OpenAI erfahren möchte, wird hier fündig werden.

Von Fabian Wüst

Er ist leidenschaftlicher Open-Source-Benutzer und ein begeisterter Technologie-Enthusiast. Als kreativer Kopf hinter Homelabtopia bringt Fabian hier seine umfangreiche Erfahrung als Linux-Admin ein. Um sicherzustellen, dass du aus seinen Beiträgen den größtmöglichen Nutzen ziehen kannst, führt er ausgiebige Tests durch und errichtet dafür immense Setups.

Schreibe einen Kommentar

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