Im Auftrag Ihrer Majestät (Linux-Magazin, Januar 2018)

Sollte mich einmal jemand in eine Fernsehshow einladen, in der ich nach Herzenslust über meine besten Produktivitätstricks schwadronieren dürfte, würde ich verkünden: Am effektivsten kommt man voran, wenn man schnell Änderungen vornimmt, die man, falls sie sich als Blindgänger herausstellen, beinahe verlustlos zurückrollen kann.

Software-Entwicklung mit Git ist so ein Beispiel: Da kann ich wagemutig mit großen Schritten neue Funktionen in meine Programme einfügen oder den Code großspurig neu arrangieren, aber falls das Ganze sich nach einer Weile als Schnapsidee herausstellt, in einer Sekunde alles wieder verwerfen, ohne dass überhaupt jemand mitbekommt, welchen Hirngespinsten ich nachgejagt bin.

Deshalb wird alles, was ich produziere, versioniert. Mein Blog, meine Artikel, meine Programme: das Versionskontrollsystem erlaubt es jederzeit, zum Stand von Gestern zurückzukehren, aus welchem Grund auch immer. Nur mit meinen Youtube-Videos klappte das bisher nicht, ändere ich zum Beispiel den Titel oder ein Tag einer meiner international geschätzten Qualitätsfilmchen (Abbildung 1), kann ich Youtube nicht mitteilen: Heute habe ich nur Blödsinn produziert, ich möchte, dass alles wieder so aussieht wie gestern.

Abbildung 1: Der Autor als pfannenkuchenwerfender Küchenchef auf Youtube.

Metadaten mit Version

Deswegen dachte ich mir, dass ich die Metadaten meiner Youtube-Existenz doch einfach einer lokalen Yaml-Datei (Listing 1) anvertrauen und diese mit Git versionieren könnte. Ein Skript ginge dann regelmäßig über die Einträge der Datei, läse die ID jedes Videos aus den Yaml-Daten, sähe auf Youtube nach, ob dort alles entsprechend der Datei eingerichtet ist, also ob der Titel in der Datei mit den von Youtube angezeigten Metadaten zum Video übereinstimmt. Bei Abweichungen passt das Skript die Youtube-Daten einfach entsprechend den Yaml-Daten an. Nichts leichter als das, oder? Listing 1 definiert in einer langen Liste eine Reihe von Videos mit ihren IDs und Titeln. Es ließe sich leicht mit Tags, Thumbnail-Image, Erscheinungsdatum, Beschreibung oder anderen Metadaten erweitern.

Listing 1: videos.yaml

    001 videos:
    002   - id: fhNAzqwy73g
    003     snippet.title: '"Virale Youtube-Videos melden", Linux Magazin 2015-01, Programmier-Snapshot, Michael Schilli'
    004     status.privacyStatus: unlisted
    005   - id: 5UKeXs13-sk
    006     snippet.title: '"Wettervorhersage für Pendler", Linux Magazin 2015-02, Programmier-Snapshot, Michael Schilli'
    007     status.privacyStatus: unlisted
    008   - id: 7fDcyfc5ifE
    009     snippet.title: '"Hotkeys auf Ubuntu mit Autokey", Linux Magazin 2015-03, Programmier-Snapshot, Michael Schilli'
    010     status.privacyStatus: unlisted
    011   - id: giPzvu-b21g
    012     snippet.title: '"Die Github-API", Linux Magazin 2015-04, Programmier-Snapshot, Michael Schilli'
    013     status.privacyStatus: unlisted
    014   - id: xMM8XjBA3A8
    015     snippet.title: '"Der Zeitgeist-Dämon auf Ubuntu", Linux Magazin 2015-05, Programmier-Snapshot, Michael Schilli'
    016     status.privacyStatus: unlisted
    017   - id: cBh3McHGKM0
    018     snippet.title: '"Das Sticky-Bit", Linux Magazin 2015-06, Programmier-Snapshot, Michael Schilli'
    019     status.privacyStatus: unlisted
    020   - id: vCUwP7sIy1M
    021     snippet.title: '"Statistischer p-Wert", Linux Magazin 2015-07, Programmier-Snapshot, Michael Schilli'
    022     status.privacyStatus: unlisted
    023   - id: D5PSGDoNdKM
    024     snippet.title: '"Kombinatorik-Rätsel", Linux Magazin 2015-08, Programmier-Snapshot, Michael Schilli'
    025     status.privacyStatus: unlisted
    026   - id: GTLMy8S8xQA
    027     snippet.title: '"Netzwerksniffer mit UI", Linux Magazin 2015-09, Programmier-Snapshot, Michael Schilli'
    028     status.privacyStatus: unlisted
    029   - id: srCrkrZRwio
    030     snippet.title: '"Configuration Management mit Ansible", Linux Magazin 2015-10, Programmier-Snapshot, Michael Schilli'
    031     status.privacyStatus: unlisted
    032   - id: Ckf6xVm7HC8
    033     snippet.title: '"Elasticsearch API", Linux Magazin 2015-11, Programmier-Snapshot, Michael Schilli'
    034     status.privacyStatus: unlisted
    035   - id: 2hKh7v6Rh-Q
    036     snippet.title: '"Etiketten drucken Dymo Labelwriter", Linux Magazin 2012-15, Programmier-Snapshot, Michael Schilli'
    037     status.privacyStatus: unlisted
    038   - id: dB-6axjWsFg
    039     snippet.title: '"Programmiertricks", Linux Magazin 2016-01, Programmier-Snapshot, Michael Schilli'
    040     status.privacyStatus: unlisted
    041   - id: saqiF40KMMA
    042     snippet.title: '"Z-Wave Controller/Switch ansteuern", Linux Magazin 2016-02, Programmier-Snapshot, Michael Schilli'
    043     status.privacyStatus: unlisted
    044   - id: 0Zr35ea1X48
    045     snippet.title: '"Garmin 64s Navidaten aufbereiten", Linux Magazin 2016-03, Programmier-Snapshot, Michael Schilli'
    046     status.privacyStatus: unlisted
    047   - id: OyS-V4vJF58
    048     snippet.title: '"WLAN-Alarme aufs Telefon", Linux Magazin 2016-04, Programmier-Snapshot, Michael Schilli'
    049     status.privacyStatus: unlisted
    050   - id: FEAVn9MkLsw
    051     snippet.title: '"Druckerfunktionstest Distro-Updates", Linux Magazin 2016-05, Programmier-Snapshot, Michael Schilli'
    052     status.privacyStatus: unlisted
    053   - id: QtcYN7TYqQ0
    054     snippet.title: '"Ebay-Abrechnungen automatisch prüfen", Linux Magazin 2016-06, Programmier-Snapshot, Michael Schilli'
    055     status.privacyStatus: unlisted
    056   - id: IG_zuoi1rSE
    057     snippet.title: '"WeMo Fernschalter mit ifttt.com", Linux Magazin 2016-07, Programmier-Snapshot, Michael Schilli'
    058     status.privacyStatus: unlisted
    059   - id: 3V67v5PrT54
    060     snippet.title: '"Mathematische Stoppprobleme", Linux Magazin 2016-08, Programmier-Snapshot, Michael Schilli'
    061     status.privacyStatus: unlisted
    062   - id: foUzO4Ga4hw
    063     snippet.title: '"Superpositions mit Perl6", Linux Magazin 2016-09, Programmier-Snapshot, Michael Schilli'
    064     status.privacyStatus: unlisted
    065   - id: gBTTAey5QHI
    066     snippet.title: '"Fahrdaten der Automatic-API auslesen", Linux Magazin 2016-10, Programmier-Snapshot, Michael Schilli'
    067     status.privacyStatus: unlisted
    068   - id: Bazeagj5R-w
    069     snippet.title: '"Datenerfassung mit Altgeräten", Linux Magazin 2016-11, Programmier-Snapshot, Michael Schilli'
    070     status.privacyStatus: unlisted
    071   - id: OaTlu3wwGmo
    072     snippet.title: '"Motion Detection mit OpenCV", Linux Magazin 2016-12, Programmier-Snapshot, Michael Schilli'
    073     status.privacyStatus: unlisted
    074   - id: YcvnpseMm1U
    075     snippet.title: '"Stromausfall im Smart Home", Linux Magazin 2017-01, Programmier-Snapshot, Michael Schilli'
    076     status.privacyStatus: public
    077   - id: lTFt9Otr5V8
    078     snippet.title: '"Amazon Web Services (AWS) Lambda (1)", Linux Magazin 2017-02, Programmier-Snapshot, Michael Schilli'
    079     status.privacyStatus: unlisted
    080   - id: 99afAACicQE
    081     snippet.title: '"Amazon Web Services (AWS) Lambda (2)", Linux Magazin 2017-03, Programmier-Snapshot, Michael Schilli'
    082     status.privacyStatus: unlisted
    083   - id: _Av1gxnQdWE
    084     snippet.title: '"Neue Alexa-Skills programmieren", Linux Magazin 2017-04, Programmier-Snapshot, Michael Schilli'
    085     status.privacyStatus: unlisted
    086   - id: oFyphTrl_Ec
    087     snippet.title: '"Textnachricht bei Login-Fehlern", Linux Magazin 2017-05, Programmier-Snapshot, Michael Schilli'
    088     status.privacyStatus: unlisted
    089   - id: 4iXRBhg8Kvg
    090     snippet.title: '"Web-Anfragen mit Scriptsprachen", Linux Magazin 2017-06, Programmier-Snapshot, Michael Schilli'
    091     status.privacyStatus: unlisted
    092   - id: ldBgrXhiryk
    093     snippet.title: '"Tensorflow und Scikit", Linux Magazin 2017-07, Programmier-Snapshot, Michael Schilli'
    094     status.privacyStatus: unlisted
    095   - id: nvHZ9CuzFRU
    096     snippet.title: '"Decision Trees", Linux Magazin 2017-08, Programmier-Snapshot, Michael Schilli'
    097     status.privacyStatus: unlisted
    098   - id: _3i5yVoTvCs
    099     snippet.title: How to flip German pancakes
    100     status.privacyStatus: public
    101   - id: rqknlsriCRQ
    102     snippet.title: La Crosse BC-700 BC-9009 fix charging completely discharged batteries LaCrosse
    103     status.privacyStatus: public
    104   - id: owPhzBXpqi8
    105     snippet.title: Door bell sensor for Smartthings hub
    106     status.privacyStatus: public
    107   - id: E164UUBY8Hk
    108     snippet.title: Pretzel baking at home with Kathi German Pretzel Mix
    109     status.privacyStatus: public
    110   - id: 2qxXhW7RxsY
    111     snippet.title: Rio Portable Beach Shelter Assembly Instructions
    112     status.privacyStatus: public
    113   - id: brPfE66FC24
    114     snippet.title: Tivo Stream Cooling Fan Replacement
    115     status.privacyStatus: public
    116   - id: SzBmCIaI0LM
    117     snippet.title: '"(Ab-) Using Round Robin Databases with Perl", Mike Schilli, at YAPC::NA 2008'
    118     status.privacyStatus: public
    119   - id: voh0HWnEgXk
    120     snippet.title: Surfer unter der Golden Gate Bridge
    121     status.privacyStatus: public
    122   - id: tuIeKuEB3ac
    123     snippet.title: BAPC Artists' Talk, Berkeley, 11-12-2008
    124     status.privacyStatus: private
    125   - id: yZj-0esWlks
    126     snippet.title: Cooking Wiener Schnitzel in San Francisco
    127     status.privacyStatus: public
    128   - id: MfNBa5aaaeE
    129     snippet.title: '"Kursverfall", Linux Magazin 2018-02, Programmier-Snapshot, Michael Schilli'
    130     status.privacyStatus: public
    131   - id: bvOvnyJTyss
    132     snippet.title: 'Scored a goal at Garfield Square!'
    133     status.privacyStatus: public
    134   - id: ssYpyXjGw9g
    135     snippet.title: 'How to fix Acura/Honda not starting in hot weather'
    136     status.privacyStatus: public
    137   - id: _Cxu3-UP0G8
    138     snippet.title: 'GIMP - Scissors Select Tool (Magnetic lasso Photoshop) - Remove thumb in picture'
    139     status.privacyStatus: public
    140   - id: R_v-2Tjl7dE
    141     snippet.title: 'Do Raisins Float In Water?'
    142     status.privacyStatus: public
    143   - id: FnHkb7sLYas
    144     snippet.title: 'Denmantau in Venice Beach'
    145     status.privacyStatus: public
    146   - id: LtrA4xuEXC4
    147     snippet.title: 'A Stormy Day in San Francisco'
    148     status.privacyStatus: public
    149   - id: LdSTIa2Tx4o
    150     snippet.title: 'Flip it: Record messages and play them Backwards'
    151     status.privacyStatus: public
    152   - id: Oma3xy1j4nY
    153     snippet.title: 'Driving a USB Rocket Launcher from Perl in User Space (2/2) (Mike Schilli, YAPC::NA 2009)'
    154     status.privacyStatus: public
    155   - id: 3px7coM0X4I
    156     snippet.title: 'Driving a USB Rocket Launcher from Perl in User Space (1/2) (Mike Schilli, YAPC::NA 2009)'
    157     status.privacyStatus: public

Im Auftrag des Users

Bevor das Skript aber auf die Userdaten zugreifen oder diese gar verändern darf, muss Google der neuen Applikation Zugriff auf die Daten des Users gewähren, schließlich dürfen nicht Hinz und Kunz mit meinen Videos herumfuhrwerken. Google holt das Einverständnis des Users ein, indem Listing 3 mittels der Google-API ([3]) den User mit dem Browser auf eine Google-Seite lotst, auf der letzterer sich einloggt und anschließend bestätigt, dass die Applikation tatsächlich Zugriffsrechte besitzt (Abbildung 2).

Abbildung 2: Beim ersten Aufruf öffnet das Skript einen Browser, der nach dem Einverständnis des Users fragt.

Hierzu legt der API-Jockey auf der Google Cloud Platform Console ([3]) zunächst ein neues Projekt an (Abbildung 3). Anschließend navigiert er zu "Create Credentials" und wählt "OAuth client ID" aus (nicht "API key", der dient nur zur Projektverwaltung).

Abbildung 3: Neue API-Keys anlegen.

Da es sich um ein Desktop-Programm und nicht um eine Web-Applikation handelt, ist im Auswahl-Menü zur Applikationsart "Other" auszuwählen. Die anschließend von Google produzierten Strings für "Client ID" und "Client Secret" (Abbildung 4) sind in eine Json-Datei nach Listing 2 einzutragen.

Abbildung 4: Ein neues Projekt namens "prog-snapshot" auf der Cloud Platform Console.

Abbildung 5: Aktive API-Schlüssel auf der Google-Console.

Nach dem ersten Lauf des Skripts, und nachdem der User im Browser den Zugriff erfolgreich bestätigt hat, verzweigt der Browser auf eine Seite mit dem Inhalt "The authentication flow has completed." und das Skript legt in der Datei oauth2.json einen OAuth2-Access-Token ab, der ihm bei zukünftigen Aufrufen Zugriff auf die Userdaten gewährt, ohne dass der User erneut einwilligen muss.

Listing 2: client-secrets.json

    1 {
    2   "installed": {
    3     "client_id": "XXX",
    4     "client_secret": "YYY",
    5     "redirect_uris": ["http://localhost", "urn:ietf:wg:oauth:2.0:oob"],
    6     "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    7     "token_uri": "https://accounts.google.com/o/oauth2/token"
    8   }
    9 }

Dies funktioniert so lange, bis der Access-Token ausläuft. Das entsprechende Verfallsdatum ist ebenfalls in der Json-Datei vermerkt. Weiter enthält die Datei einen Refresh-Token, mit dem das Skript nach Ablauf der Gültigkeit des Access-Tokens einen frischen anfordern kann, was praktisch endlos funktioniert, es sei denn, der User begibt sich auf die Google Console und entzieht dem Client den Zugriff, dann dreht Google den Hahn zu.

Listing 3: youtube-sync

    01 #!/usr/bin/python
    02 import httplib2
    03 import os
    04 import sys
    05 import yaml
    06 
    07 from apiclient.discovery import build
    08 from apiclient.errors import HttpError
    09 from oauth2client.client import \
    10   flow_from_clientsecrets
    11 from oauth2client.file import Storage
    12 from oauth2client.tools import \
    13   argparser, run_flow
    14 
    15 CLIENT_SECRETS_FILE = "client-secrets-local.json"
    16 YOUTUBE_READ_WRITE_SCOPE = \
    17   "https://www.googleapis.com/auth/youtube"
    18 YOUTUBE_API_SERVICE_NAME = "youtube"
    19 YOUTUBE_API_VERSION      = "v3"
    20 
    21 def get_authenticated_service(args):
    22   flow = flow_from_clientsecrets(
    23     CLIENT_SECRETS_FILE,
    24     scope=YOUTUBE_READ_WRITE_SCOPE)
    25 
    26   storage = Storage("oauth2.json");
    27   credentials = storage.get()
    28 
    29   if credentials is None or \
    30      credentials.invalid:
    31     credentials = \
    32             run_flow(flow, storage, args)
    33 
    34   return build(YOUTUBE_API_SERVICE_NAME,
    35     YOUTUBE_API_VERSION,
    36     http=credentials.authorize(
    37         httplib2.Http()))
    38 
    39 def video_update(youtube, id, title):
    40   response = youtube.videos().list(
    41     id=id, part='snippet').execute()
    42 
    43   if not response["items"]:
    44     print("Video '%s' was not found." % id)
    45     sys.exit(1)
    46 
    47   snippet = response["items"][0]["snippet"]
    48 
    49   if snippet['title'] == title:
    50     print("%s: Unchanged" % id)
    51     return
    52 
    53   snippet['title'] = title
    54 
    55   try:
    56     youtube.videos().update(
    57       part='snippet',
    58       body=dict(
    59          snippet=snippet, id=id)).execute()
    60   except HttpError, e:
    61     print("HTTP error %d: %s" % \
    62             (e.resp.status, e.content))
    63   else:
    64     print("Updated OK")
    65 
    66 if __name__ == "__main__":
    67   args = argparser.parse_args()
    68   youtube = get_authenticated_service(args)
    69 
    70   stream = open("videos.yaml", "r")
    71   all = yaml.load(stream)
    72   for video in all['videos']:
    73     video_update(youtube, video['id'],
    74             video['title'])

Die Funktion get_authenticated_service() ab Zeile 21 in Listing 3 definiert als "Scope" also als Reichweite des Zugriffs YOUTUBE_READ_WRITE_SCOPE, fordert also Lese- sowie Schreibrechte an. Die Interaktion mit dem Browser und den dahintersteckenden Oauth2-Tokentanz hat Google schön im SDK abstrahiert, das Skript ruft lediglich die Funktionen flow_from_clientsecrets() und run_flow() aus den Paketen oauth2client.client und oauth2client.tools auf. Das Ringelreihen mit dem Browser funkioniert sowohl auf Linux als auch auf dem Mac erstaunlicherweise gleich gut.

Zeile 69 liest die Yaml-Datei mit den lokal gehaltenen Video-Metadaten ein und Zeile 71 iteriert über alle dort gefundenen Videos. Für jedes ruft es die ab Zeile 39 definierte Funktion video_update() auf, die mit youtube.videos() zunächst Metadaten des mittels seiner ID bezeichneten Videos des Users einholt, und beschränkt sich auf den Bereich "snippet", womit Youtube Json-Daten mit Titel, Tags, Beschreibung und einigen Feldern mehr bezeichnet. Aus diesen Metadaten holt Zeile 49 den Titel des Videos und vergleicht ihn mit der lokalen Version. Stimmen beide nicht überein, stößt Zeile 56 in einem Try-Block die update()-Methode an, die als Parameter die ursprünglich eingeholten Metadaten mit dem angepassten neuen Titel beigepackt erhalten. Tritt ein Fehler beim Übertragen auf, druckt Zeile 61 die HTTP-Fehlermeldung, sonst meldet Zeile 64 "Updated OK" und die Metadaten auf Youtube entsprechen nun den lokal gehaltenen im Versionierungssystem.

Installation und Ausblick

Das zur Inbetriebnahme des Skripts notwendige SDK ist als Python-Paket im Standard-Repository erhältlich und kann mit pip installiert werden:

    $ pip install --upgrade google-api-python-client

Nach dem ersten Lauf des Skripts, das das eingangs gezeigte Browserfenster aufspannt und das Einverständnis des Users einholt, zeigt das Skript mit der Yaml-Datei in Listing 1 die folgende Ausgabe:

    $ ./youtube-sync 
    _3i5yVoTvCs: Unchanged
    brPfE66FC24: Unchanged
    2qxXhW7RxsY: Unchanged

Da alle Titel-Strings in den frisch eingeholten Youtube-Metadaten mit den Yaml-Daten übereinstimmen, nimmt das Skript keinerlei Anpassungen vor. Ändert sich allerdings ein Titeltext, pumpt das Skript diesen zu Youtube, das die Metadaten des Videos auffrischt. Das Skript bestätigt dies mit "Updated OK".

Das Skript lässt sich nun beliebig erweitern, so kann es auch Videos auf der Festplatte bei Bedarf hochladen und so den lokalen Status der Filmsammlung mit jedem Lauf automatisch mit den öffentlich zugänglichen Videos auf Youtube abgleichen. Versioniert der User die lokale Sammlung und ihre Metadaten mit einem Versionskontrollsystem wie Git, kann er zeitlich vor- und wieder zurückspringen, wagemutig Änderungen vornehmen und sie sofort wieder zurückrollen, falls sich eine Idee mal als nicht so glorreich erweist.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2018/01/snapshot/

[2]

Michael Schilli, "Titel": Linux-Magazin 12/16, S.104, <U>http://www.linux-magazin.de/Ausgaben/2016/12/Perl-Snapshot<U>

[3]

Google Cloud Platform Console: https://console.cloud.google.com/apis

[4]

"OAuth 2.0 for Mobile & Desktop Apps", https://developers.google.com/identity/protocols/OAuth2InstalledApp

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc