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