Um Online-Dokumente auf Google Drive mit Tags zu versehen, legt ein Skript unter Nutzung zweier APIs Metadaten auf Evernote ab.
Wie vor zwei Monaten hier im Snapshot ([2]) erörtert, liegen meine ehemaligen Papierbücher jetzt als PDFs auf Google Drive. Sitze ich aber vor meinem mobilen Lesegerät und möchte in der Online-Bibliothek schmökern, ist die lange textbasierte Bücherliste in Abbildung 1 wenig hilfreich. Was hier noch fehlt, ist ein Mechanismus, mit dem ich zum Beispiel gelesene Bücher als solche markieren kann. Weiter eignen sich manche großformatige Bücher wie Informatikwerke mit seitenlangen Programm-Listings nicht für Mobiltelefone, und wenn ich an der Supermarktkasse Schlange stehe, will ich aus kleinformatigen Werken wählen, die das kleine Smartphone-Display auch anzeigen kann.
Abbildung 1: Gescannte Papierbücher als PDFs auf Google Drive erscheinen leider nur als graue Textlisten. |
Abbildung 2: Mit Hilfe eines API-Skripts zeigt Evernote die Bücher der Bibliothek mit Titelbild und Tagging-Funktion. |
Auf Google Drive fehlen also Tags, die Dateien mit Eigenschaften versehen. Ein "finished"-Tag für ausgelesene Werke, ein "active"-Tag für halb gelesene, und ein "pocket"-Tag für Taschenbücher, die auch auf einem Smartphone gut zu lesen sind. Dann könnte man entsprechend aktueller Anforderungen nur Bücher mit bestimmten Tags auflisten und aus der beschränkten Auswahl viel zügiger auswählen.
Bis die Google-Entwickler in die Hufe kommen, fiel deshalb meine Wahl auf die Applikation Evernote. Sie ist auf allen Geräten verfügbar, auf denen auch Google-Drive läuft, und speichert, wie vor einiger Zeit hier erörtert ([3] und [4]) beliebige Daten in "Note" genannten Einträgen ab, die wiederum in "Notebooks" genannten Ordnern liegen. Außerdem bietet Evernote eine API auf den Datenspeicher an, sodass man ohne weiteres mal kurz hunderte oder tausende Einträge maschinengesteuert anlegen kann.
Die gewünschten Metadaten legt Evernote einfach als Tags ab, die es Notes zuweist. Der Anwender vergibt Tags entweder mit der Maus in einem Webbrowser auf evernote.com oder mittels einer der vielen Clients auf Windows-, Mac- oder Mobilgeräten. Auch die API weist Notes auf Wunsch ebenfalls per API generierte Tags zu.
Wie kommen nun die Einträge für die PDF-Dateien in die
Evernote-Datenbank? Das heute vorgestellte Skript in Listing 1 legt von
allen lokal gefundenen
PDF-Dateien ein JPG-Bild der Titelseite an und lädt es in einen
Note-Eintrag im Folder 50-Ebooks
meines Evernote-Accounts hoch. Schiebe ich
dann grafisch mit der Maus
Tags auf die Einträge, kann ich mit Evernotes Suchfunktion
(Abbildung 4) anschließend nur Einträge mit bestimmten Tags auswählen.
Abbildung 3: Im Browser hat der User dem Buch "Der App-Entwickler Crashkurs" die Tags "computer-book" und "finished-book" zugewiesen. |
Abbildung 4: Sind die Tags "fun-book" und "pocket-book" gesetzt, zeigt die Suche nur unterhaltsame Taschenbücher. |
Seit dem Erscheinen der Perl-Snapshots, die die Evernote-API nutzten ([3] und [4]) hat Evernote von Passwort-basierter Authentisierung auf OAuth2 umgestellt. Folglich funktionieren die Skripts von damals nicht mehr. Der Aufwand für neue Applikationen stieg damit leicht an, allerdings hatten die Evernote-Entwickler ein Einsehen mit Hobbybastlern, die statt Daten von wildfremden App-Kunden nur ihre eigenen Accounts manipulieren wollen. Freizeitschrauber können sich nämlich unter [5] einen sogenannten "Developer-Token" abholen, einen Textstring, der sich lokal speichern lässt und ähnlich wie ein Passwort Zugang zu einem einzigen Evernote-Datenspeicher gewährt.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Net::Evernote::Simple; 004 use Log::Log4perl qw(:easy); 005 use File::Basename; 006 use File::Temp qw( tempfile ); 007 use Digest::MD5 qw( md5_hex ); 008 use Sysadm::Install qw( :all ); 009 010 Log::Log4perl->easy_init( $INFO ); 011 012 my( $HOME ) = glob "~"; 013 014 my $EN_FOLDER = "50-Ebooks"; 015 my $BOOKS_DIR = "$HOME/books"; 016 017 my $enote = Net::Evernote::Simple->new(); 018 my $enstore = $enote->note_store(); 019 020 my $en_folder_id = en_folder_id( 021 $enote, $enstore ); 022 023 my %en_books = map { $_ => 1 } 024 en_folder_notes( $enote, $enstore, 025 $en_folder_id ); 026 027 for my $pdf ( <$BOOKS_DIR/*.pdf> ) { 028 my $file = basename $pdf; 029 (my $title = $file ) =~ s/\.pdf$//; 030 031 if( exists $en_books{ $title } ) { 032 DEBUG "$title already in Evernote"; 033 next; 034 } 035 036 en_add( $enote, $enstore, $title, 037 title_pic( $pdf ), $en_folder_id ); 038 } 039 040 ########################################### 041 sub en_add { 042 ########################################### 043 my( $enote, $enstore, $title, 044 $title_pic, $en_folder_id ) = @_; 045 046 my $PRFX = "Net::Evernote::Simple::" . 047 "EDAMTypes::"; 048 my $data_class = $PRFX . "Data"; 049 my $resource_class = $PRFX . "Resource"; 050 my $note_class = $PRFX . "Note"; 051 052 eval "require $data_class"; 053 eval "require $resource_class"; 054 eval "require $note_class"; 055 056 INFO "Adding $title to Evernote"; 057 058 my $data = $data_class->new(); 059 060 my $content = slurp $title_pic; 061 $data->body( $content ); 062 my $hash = md5_hex( $content ); 063 $data->bodyHash( $hash ); 064 $data->size( length $content ); 065 066 my $r = $resource_class->new(); 067 $r->data( $data ); 068 $r->mime( "image/jpeg" ); 069 $r->noteGuid( "" ); 070 071 my $note = $note_class->new(); 072 $note->title( $title ); 073 $note->resources( [$r] ); 074 $note->notebookGuid( $en_folder_id ); 075 076 my $enml = <<EOT; 077 <?xml version="1.0" encoding="UTF-8"?> 078 <!DOCTYPE en-note SYSTEM 079 "http://xml.evernote.com/pub/enml2.dtd"> 080 <en-note> 081 <en-media type="image/jpeg" 082 hash="$hash"/> 083 </en-note> 084 EOT 085 086 $note->content( $enml ); 087 088 $enstore->createNote( 089 $enote->dev_token(), $note ); 090 } 091 092 ########################################### 093 sub title_pic { 094 ########################################### 095 my( $in_pdf ) = @_; 096 097 my ($fh1, $pdf_file) = tempfile( 098 SUFFIX => '.pdf', UNLINK => 1 ); 099 100 my ($fh2, $jpg_file) = tempfile( 101 SUFFIX => '.jpg', UNLINK => 1 ); 102 103 eval { 104 tap { raise_error => 1 }, 105 "pdftk", "A=$in_pdf", "cat", "A1", 106 "output", $pdf_file; 107 108 tap { raise_error => 1 }, 109 "convert", "-resize", "100x", 110 $pdf_file, $jpg_file; 111 }; 112 113 return $jpg_file; 114 } 115 116 ########################################### 117 sub en_folder_id { 118 ########################################### 119 my( $enote, $store ) = @_; 120 121 my $notebooks = $enstore->listNotebooks( 122 $enote->dev_token() ); 123 124 for my $notebook (@$notebooks) { 125 if ( $notebook->name() eq $EN_FOLDER ){ 126 return $notebook->guid(); 127 } 128 } 129 130 die "$EN_FOLDER not found"; 131 } 132 133 ########################################### 134 sub en_folder_notes { 135 ########################################### 136 my( $enote, $store, $en_folder_id ) = @_; 137 138 my $filter_class = "Net::Evernote::" . 139 "Simple::EDAMNoteStore::NoteFilter"; 140 eval "require $filter_class"; 141 142 my $filter = $filter_class->new(); 143 $filter->notebookGuid( $en_folder_id ); 144 145 my @titles = (); 146 147 my $max_per_call = 50; 148 149 for( my $offset = 0; ; 150 $offset += $max_per_call ) { 151 152 my $note_list = $store->findNotes( 153 $enote->dev_token(), 154 $filter, $offset, $max_per_call ); 155 156 my $notes_found = 0; 157 158 for my $note ( 159 @{ $note_list->{ notes } } ) { 160 $notes_found++; 161 162 push @titles, $note->title(); 163 } 164 165 last if $notes_found != $max_per_call; 166 } 167 168 return @titles; 169 }
Listing 1 zieht als wichtigstes CPAN-Modul Net::Evernote::Simple herein,
das der Einfachheit halber die offizielle Evernote-Thrift-API schon
beinhaltet. Eine YAML-Datei namens ~/.evernote.yml
im Home-Verzeichnis
des Users enthält unter dem Eintrag dev_token
den Developer Token, den
der auf Evernote eingeloggte User unter [5] abholen kann. In Zeile 18
liefert dann die Methode note_store()
ein Objekt, mit dem der User
direkt Evernote-API-Funktionen aufrufen kann, die intern als Web-Requests
an den Evernote-Server geschickt werden.
Zeile 20 ermittelt über die Funktion en_folder_id
die GUID des
Notebooks 50-Ebooks
, das der User vorher auf Evernote angelegt hat.
Ab Zeile 115 ruft sie zunächst die API-Funktion listNotebooks()
auf,
übergibt den Developer-Token, und erhält eine Liste aller Notebooks des
Accounts. Die if-Bedingung in Zeile 123 filtert das gesuchte heraus.
Die Methode guid()
extrahiert daraus die GUID und gibt sie ans
Hauptprogramm zurück.
Bei jedem Aufruf holt das Skript mit en_folder_notes()
alle im Notebook 50-Ebooks
auf Evernote gespeicherten Notes ab, iteriert
anschließend über die lokal gespeicherten PDF-Dateien mit den Ebooks
(ginge natürlich auch mit Cloud-PDFs auf Google Drive) und findet so heraus,
welche lokalen PDFs noch nicht in Evernote liegen. Für jedes so gefundene
Dokument legt der Aufruf der Funktion en_add()
in Zeile 36 eine neue
Evernote-Note an. Die Funktion en_folder_notes()
legt zum Finden
der Einträge in einem Notebook einen Filter an, der die Notebook-GUID
vorgibt und holt dann jeweils 50 Einträge auf einmal ab. Kommen 50
zurück, erhöht es den Zähler $offset
und fragt nochmal nach.
Da Net::Evernote::Simple die Evernote-API enthält, muss es sie
im Modulraum mit dem gleichnamigen Präfix versehen. Modulnamen
wie EDAMTypes::Data werden dem vorangestellten Präfix etwas länglich,
und wegen der beschränkten Spaltenbreite im Heft stellen die Zeilen
46-54 die erforderlichen Modulnamen der API zeilenweise zusammen und
ziehen dann die Module mit require
herein. In einem normalen Skript
nutzt man einfach "use Net::...::Data".
Das neben dem Namen der PDF-Datei ebenfalls benötigte JPG-Bild der
Titelseite fieselt die Utility pdftk
in der Funktion title_pic
heraus und convert
aus dem ImageMagick-Fundus wandelt die
PDF-Titelseite in Zeile 107 in ein JPG-Foto um. Beide Utilities lassen
sich mit einem Package-Installer wie zum Beispiel apt-get
installieren.
Das Anlegen einer neuen Note in Evernote mit einer eingebetteten JPG-Datei
erfordert etwas XML, wie die Funktion en_add()
ab Zeile 41 zeigt. Zunächst
schlürft die Funktion slurp
aus dem Fundus des CPAN-Moduls
Sysadm::Install die JPG-Daten in die Variable $content
.
Diese Rohdaten nimmt die Methode body()
der
Klasse EDAMTypes::Data entgegen und verlangt außerdem einen
MD5-Hash des Inhalts, den die Funktion md5_hash()
aus dem
CPAN-Modul Digest::MD5 generiert. Auch die Größe der Datei in
Bytes muss explizit eingetütet werden.
Aus dieser Datenklasse formen die Zeilen 66-69 ein Objekt der
Klasse EDAMTypes::Resource, die die Methode resources()
wiederum in ein
Objekt der Klasse EDAMTypes::Note eintütet. Jede Evernote-Notiz
besteht aus einer Anzahl dieser Resourcen, auf die die Notes dann
verlinken, damit Browser und Evernote-Applikationen
sie als Teil der Note anzeigen können. Schließlich stopft noch
Zeile 86 das weiter oben angegebene XML zur Formatierung des
Inhalts in den Content-Bereich der Note und ein abschließender
Aufruf der API-Funktion createNote()
mit dem Developer-Token spielt
die neue Note auf dem Evernote-Server ein.
Mit der in [2] vorgstellten Google-Drive-API lässt sich in Listing 1
dann noch leicht ein Download-Link in jeden Evernote-Eintrag einbauen,
der auf das PDF-Dokument auf dem Google-Drive zeigt.
Dazu liefert die API für gefundene Dateien ein Feld namens "downloadUrl",
das einen https-Link auf den Google Server enthält. In ihm ist der
Query-Parameter gd=true
gesetzt. Entfernt man ihn, lädt ein Browserklick
die Drive-Datei auf das lokal verwendete Gerät herunter.
Zur Performance-Optimierung
lassen
sich die Dokumente wie in [2] vorgeschlagen auch aufgesplittet ablegen und
entsprechend verlinken. So kann der Lesewütige auch von Unterwegs die
Werkte häppchenweise zügig herunterladen und gleich loslesen.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2013/02/Perl
"Papierbuch am Ende", Michael Schilli, Linux-Magazin 12/2012, http://www.linux-magazin.de/Ausgaben/2012/12/Perl-Snapshot
"Zettels Trauma", Michael Schilli, Linux-Magazin 01/2012, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2012/01/Perl-Snapshot
"Unvergesslich", Michael Schilli, Linux-Magazin 04/2012, http://www.linux-magazin.de/Ausgaben/2012/04/Perl-Snapshot
Evernote gibt "Developer Tokens" zum vereinfachten privaten Datenzugriff aus: http://dev.evernote.com/documentation/cloud/chapters/Authentication.php#devtoken