Wie vor zwei Monaten an gleicher Stelle [2] erörtert, liegen meine ehemaligen Papierbücher jetzt als PDFs auf Google Drive. Die Liste meiner digitalisierten Folianten am PC-Bildschirm zu durchstöbern ist kein Problem. Sitze ich aber vor meinem mobilen Lesegerät und möchte in der Onlinebibliothek schmökern, ist die lange Textliste in Abbildung 1 unpraktisch. Was fehlt, ist ein Mechanismus, mit dem ich zum Beispiel gelesene Bücher als solche markieren kann.
Abbildung 1: Michael Schillis Bibliothek auf Google Drive erscheint bislang nur als graue PDF-Liste gescannter Papierbücher.
Helfen würde es auch, großformatige Bücher – Informatik-Literatur mit langen Programmlistings beispielsweise – extra zu kennzeichnen, da ihre Darstellung auf Mobiltelefonen mies ist. Wenn ich an der Supermarktkasse Schlange stehe, will ich auf dem Smartphone am liebsten nur kleinformatige Werke angeboten bekommen, mit denen das Minidisplay am besten klarkommt.
Auf Google Drive fehlen also Tags, die Dateien mit Eigenschaften versehen. Ein »finished«
-Tag für ganz gelesene 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 jeder Benutzer nach seinen Vorlieben Bücher mit bestimmten Tags auflisten und zügig auswählen.
Bis die Google-Entwickler in die Gänge kommen, fällt 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], [4]), beliebige Daten in "Note" genannten Einträgen, die wiederum in "Notebooks" genannten Ordnern liegen. Außerdem bietet der Evernote-Dienst ein API auf den Datenspeicher an, sodass Programmierer ohne Weiteres Hunderte oder Tausende Einträge maschinengesteuert anlegen können.
Die gewünschten Metadaten legt Evernote als Tags ab, die es Notes zuweist. Der Anwender vergibt Tags entweder mit der Maus im Webbrowser auf Evernote.com (Abbildung 3) oder mit einem der vielen Clients auf Windows-, Mac- oder Mobilgeräten. Auch das API weist Notes API-generierte Tags zu.
Bleibt zu klären, wie die Einträge für die PDF-Dateien in die Evernote-Datenbank kommen. Das vorgestellte Skript (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 (Abbildung 2). Nachdem ich mit der Maus Tags auf die Einträge gezogen habe, kann ich mit Evernotes Suchfunktion Einträge auswählen (Abbildung 4).
Abbildung 2: Mit Hilfe eines API-Skripts zeigt Evernote die Bücher der Bibliothek nun mit Titelbild und Tagging-Funktion.
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.
Einfacher als Oauth
Nach dem Erscheinen der Perl-Snapshots ([3], [4) hat Evernote leider von Passwort-basierter Authentisierung auf Oauth 2 umgestellt. Wegen des geänderten API funktionieren die Skripte von damals nicht mehr und neue Applikationen zu entwickeln gestaltet sich etwas aufwändiger als zuvor.
Doch haben die Evernote-Entwickler ein Einsehen mit Hobbybastlern, die statt Daten von wildfremden App-Kunden nur ihre eigenen Accounts manipulieren wollen. Freizeitschrauber dürfen sich nämlich unter [5] einen so genannten Developer-Token abholen. Der Textstring lässt sich lokal speichern und gewährt ähnlich wie ein Passwort Zugang zu genau einem Evernote-Datenspeicher.
Listing 1 zieht als wichtigstes CPAN-Modul Net::Evernote::Simple herein, das der Einfachheit halber das offizielle Evernote-Thrift-API schon enthält. Eine Yaml-Datei namens »~/.evernote.yml«
im Homeverzeichnis des Users enthält unter dem Eintrag »dev_token«
den Developer-Token, den sich der auf Evernote eingeloggte User unter [5] abgeholt hat. In Zeile 18 liefert die Methode »note_store()«
ein Objekt, mit dem der User direkt Evernote-API-Funktionen aufrufen darf, die der Evernote-Server intern als Webrequests geschickt bekommt.
Zeile 20 ermittelt über »en_folder_id()«
die GUID des Notebooks »50-Ebooks«
, das der User auf Evernote schon 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.
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 tap { raise_error => 1 },
104 "pdftk", "A=$in_pdf", "cat", "A1",
105 "output", $pdf_file;
106
107 tap { raise_error => 1 },
108 "convert", "-resize", "100x",
109 $pdf_file, $jpg_file;
110
111 return $jpg_file;
112 }
113
114 ###########################################
115 sub en_folder_id {
116 ###########################################
117 my( $enote, $store ) = @_;
118
119 my $notebooks = $enstore->listNotebooks(
120 $enote->dev_token() );
121
122 for my $notebook (@$notebooks) {
123 if ( $notebook->name() eq $EN_FOLDER ){
124 return $notebook->guid();
125 }
126 }
127
128 die "$EN_FOLDER not found";
129 }
130
131 ###########################################
132 sub en_folder_notes {
133 ###########################################
134 my( $enote, $store, $en_folder_id ) = @_;
135
136 my $filter_class = "Net::Evernote::" .
137 "Simple::EDAMNoteStore::NoteFilter";
138 eval "require $filter_class";
139
140 my $filter = $filter_class->new();
141 $filter->notebookGuid( $en_folder_id );
142
143 my @titles = ();
144
145 my $max_per_call = 50;
146
147 for( my $offset = 0; ;
148 $offset += $max_per_call ) {
149
150 my $note_list = $store->findNotes(
151 $enote->dev_token(),
152 $filter, $offset, $max_per_call );
153
154 my $notes_found = 0;
155
156 for my $note (
157 @{ $note_list->{ notes } } ) {
158 $notes_found++;
159
160 push @titles, $note->title();
161 }
162
163 last if $notes_found != $max_per_call;
164 }
165
166 return @titles;
167 }
Schrittweise abpumpen
Bei jedem Aufruf holt das Skript mit »en_folder_notes()«
alle im Notebook »50-E-books«
auf Evernote gespeicherten Notes ab und iteriert über die lokal gespeicherten E-Book-PDFs. Das Gleiche ließe sich auch mit Cloud-PDFs auf Google Drive erreichen. Wie auch immer, das Programm findet raus, welche lokalen PDFs noch nicht in Evernote liegen.
Für jedes gefundene Dokument legt die in Zeile 36 aufgerufene Funktion »en_add()«
eine neue Evernote-Note an. Die Funktion »en_folder_notes()«
erzeugt zum Finden der Einträge in einem Notebook einen Filter, der die Notebook-GUID vorgibt, und holt dann jeweils 50 Einträge auf einmal ab. Kommen tatsächlich 50 zurück, erhöht sie den Zähler »$offset«
und fragt abermals nach.
Da Net::Evernote::Simple das Evernote-API enthält, muss die Library im Modulraum das gleichnamige Präfix vorsehen. Modulnamen wie EDAMTypes::Data geraten wegen des führenden Präfix etwas länglich. Da die Spaltenbreite beim gedruckten Listing 1 begrenzt ist, stellen die Zeilen 46 bis 54 die erforderlichen Modulnamen des API zeilenweise zusammen und ziehen dann die Module mit »require«
herein. Normale Skripte würden einfach »use Net::... ::Data«
benutzen.
Das neben dem Bücher-Dateinamen ebenfalls benötigte Jpg-Bild der Titelseite fieselt die Funktion »title_pic()«
unter tätiger Mithilfe des Utility »pdftk«
aus den PDF-Dateien heraus. »convert«
aus dem Imagemagick-Fundus wandelt die PDF-Titelseite in Zeile 107 in ein Jpg-Foto um. Beide Utilities sind mit einem Paketmanager wie »apt-get«
schnell installiert.
Das Anlegen einer neuen Note in Evernote mit einer eingebetteten Jpg-Datei erfordert etwas XML, wie »en_add()«
ab Zeile 41 nahelegt. 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 herzustellen in der Lage ist. Auch die Dateigröße in Bytes ist einzutüten.
Aus der Datenklasse formen die Zeilen 66 bis 69 ein Objekt der Klasse EDAMTypes::Resource. Diese wiederum packt die Methode »resources()«
in ein Objekt der Klasse EDAMTypes::Note. Jede Evernote-Notiz besteht aus einer Anzahl dieser Ressourcen, auf welche die Notes verlinken, damit Browser und Evernote-Applikationen sie als Teil der Note anzeigen können. Am Ende stopft die Zeile 86 das weiter oben angegebene XML in den Contentbereich der Note, damit die Inhalte ein Format bekommen.
Ein abschließender Aufruf der API-Funktion »createNote()«
mit dem Developer-Token spielt die neue Note auf dem Evernote-Server ein.
In einem Screencast demonstriert Michael Schilli das Beispiel: http://www.linux-magazin.de/plus/2013/02