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. |
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.
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
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.
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.
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.
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.
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2018/01/snapshot/
Michael Schilli, "Titel": Linux-Magazin 12/16, S.104, <U>http://www.linux-magazin.de/Ausgaben/2016/12/Perl-Snapshot<U>
Google Cloud Platform Console: https://console.cloud.google.com/apis
"OAuth 2.0 for Mobile & Desktop Apps", https://developers.google.com/identity/protocols/OAuth2InstalledApp
Hey! The above document had some coding errors, which are explained below:
Unknown directive: =desc