source: extensions/piwigo_import_tree/piwigo_import_tree.pl @ 30840

Last change on this file since 30840 was 27797, checked in by plg, 11 years ago

version 1.0: compatible with Piwigo 2.6

File size: 13.8 KB
Line 
1#!/usr/bin/perl
2
3# author: Pierrick Le Gall (plg)
4# version: 1.0
5# documentation: http://piwigo.org/doc/doku.php?id=user_documentation:tools:piwigo_import_tree
6#
7# usage:
8# perl piwigo_import_tree.pl
9#    --base_url=http://address/of/your/piwigo
10#    --user=admin_username
11#    --password=??
12#    --directory="my photos directory"
13#    [--parent_album_id=NN]
14#    [--quiet]
15#    [--only_write_cache]
16#    [--reload_properties]
17#    [--debug]
18#    [--short_lines]
19
20use strict;
21use warnings;
22
23# make it compatible with Windows, but breaks Linux
24#use utf8;
25
26use File::Find;
27use Data::Dumper;
28use File::Basename;
29use LWP::UserAgent;
30use JSON;
31use Getopt::Long;
32use Encode qw/is_utf8 decode/;
33use Time::HiRes qw/gettimeofday tv_interval/;
34use Digest::MD5 qw/md5 md5_hex/;
35
36my %opt = ();
37GetOptions(
38    \%opt,
39    qw/
40          base_url=s
41          username=s
42          password=s
43          directory=s
44          parent_album_id=s
45          define=s%
46          quiet
47          only_write_cache
48          reload_properties
49          debug
50          short_lines
51      /
52);
53
54my $album_dir = $opt{directory};
55$album_dir =~ s{^\./*}{};
56
57our $ua = LWP::UserAgent->new;
58$ua->agent('Mozilla/piwigo_remote.pl 1.25');
59$ua->cookie_jar({});
60
61my %conf;
62my %conf_default = (
63    base_url => 'http://localhost/plg/piwigo/salon',
64    username => 'plg',
65    password => 'plg',
66);
67
68foreach my $conf_key (keys %conf_default) {
69    $conf{$conf_key} = defined $opt{$conf_key} ? $opt{$conf_key} : $conf_default{$conf_key}
70}
71
72$ua->default_headers->authorization_basic(
73    $conf{username},
74    $conf{password}
75);
76
77my $result = undef;
78my $query = undef;
79
80binmode STDOUT, ":encoding(utf-8)";
81
82# Login to Piwigo
83piwigo_login();
84
85# Fill an "album path to album id" cache
86my %piwigo_albums = ();
87
88my $response = $ua->post(
89    $conf{base_url}.'/ws.php?format=json',
90    {
91        method => 'pwg.categories.getList',
92        recursive => 1,
93        fullname => 1,
94    }
95);
96
97my $albums_aref = from_json($response->content)->{result}->{categories};
98foreach my $album_href (@{$albums_aref}) {
99    $piwigo_albums{ $album_href->{name} } = $album_href->{id};
100}
101# print Dumper(\%piwigo_albums)."\n\n";
102
103if (defined $opt{parent_album_id}) {
104    foreach my $album_path (keys %piwigo_albums) {
105        if ($piwigo_albums{$album_path} == $opt{parent_album_id}) {
106            $conf{parent_album_id} = $opt{parent_album_id};
107            $conf{parent_album_path} = $album_path;
108        }
109    }
110
111    if (not defined $conf{parent_album_path}) {
112        print "Parent album ".$opt{parent_album_id}." does not exist\n";
113        exit();
114    }
115}
116
117# Initialize a cache with file names of existing photos, for related albums
118my %photos_of_album = ();
119
120# Synchronize local folder with remote Piwigo gallery
121find({wanted => \&add_to_piwigo, no_chdir => 1}, $album_dir);
122
123#---------------------------------------------------------------------
124# Functions
125#---------------------------------------------------------------------
126
127sub piwigo_login {
128    $ua->post(
129        $conf{base_url}.'/ws.php?format=json',
130        {
131            method => 'pwg.session.login',
132            username => $conf{username},
133            password => $conf{password},
134        }
135    );
136}
137
138sub fill_photos_of_album {
139    my %params = @_;
140
141    if (defined $photos_of_album{ $params{album_id} }) {
142        return 1;
143    }
144
145    piwigo_login();
146    my $response = $ua->post(
147        $conf{base_url}.'/ws.php?format=json',
148        {
149            method => 'pwg.categories.getImages',
150            cat_id => $params{album_id},
151            per_page => 1000000,
152        }
153    );
154
155    foreach my $image_href (@{from_json($response->content)->{result}{images}}) {
156        $photos_of_album{ $params{album_id} }{ $image_href->{file} } = $image_href->{id};
157    }
158}
159
160sub photo_exists {
161    my %params = @_;
162
163    fill_photos_of_album(album_id => $params{album_id});
164
165    if (defined $photos_of_album{ $params{album_id} }{ $params{file} }) {
166        return $photos_of_album{ $params{album_id} }{ $params{file} };
167    }
168    else {
169        return 0;
170    }
171}
172
173sub add_album {
174    my %params = @_;
175
176    my $form = {
177        method => 'pwg.categories.add',
178        name => $params{name},
179    };
180
181    if (defined $params{parent}) {
182        $form->{parent} = $params{parent};
183    }
184
185    piwigo_login();
186    my $response = $ua->post(
187        $conf{base_url}.'/ws.php?format=json',
188        $form
189    );
190
191    return from_json($response->content)->{result}{id};
192}
193
194sub set_album_properties {
195    my %params = @_;
196
197    print '[set_album_properties] for directory "'.$params{dir}.'"'."\n" if $opt{debug};
198
199    # avoid to load the readme.txt file 10 times if an album has 10
200    # sub-albums
201    our %set_album_properties_done;
202    if (defined $set_album_properties_done{ $params{id} }) {
203        print '[set_album_properties] already done'."\n" if $opt{debug};
204        return;
205    }
206    $set_album_properties_done{ $params{id} } = 1;
207
208    $params{dir} =~ s{ / }{/}g;
209
210    # is there a file "readme.txt" in the directory of the album?
211    my $desc_filepath = $params{dir}.'/readme.txt';
212
213    if (not -f $desc_filepath) {
214        print "no readme.txt for ".$params{dir}."\n" if $opt{debug};
215        return;
216    }
217
218    # example of readme.txt:
219    #
220    # Title: First public opening
221    # Date: 2009-09-26
222    # Copyright: John Connor
223    #
224    # Details:
225    # The first day Croome Court is opened to the public by the National Trust.
226    # And here is another line for details!
227
228    open(IN, '<', $desc_filepath);
229    my $title = undef;
230    my $date_string = undef;
231    my $copyright = undef;
232    my $is_details = 0;
233    my $details = '';
234    while (my $desc_line = <IN>) {
235        chomp($desc_line);
236
237        if ($is_details) {
238            $details.= $desc_line;
239        }
240        elsif ($desc_line =~ /^Date:\s*(.*)$/) {
241            $date_string = $1;
242        }
243        elsif ($desc_line =~ /^Title:\s*(.*)$/) {
244            $title = $1;
245        }
246        elsif ($desc_line =~ /^Copyright:\s*(.*)$/) {
247            $copyright = $1;
248        }
249        elsif ($desc_line =~ /^Details:/) {
250            # from now, all the remaining lines are "details"
251            $is_details = 1;
252        }
253    }
254    close(IN);
255
256    if (defined $date_string or $details ne '') {
257        my $comment = '';
258
259        if (defined $date_string) {
260            $comment.= '<span class="albumDate">'.$date_string.'</span><br>';
261        }
262        if (defined $copyright) {
263            $comment.= '<span class="albumCopyright">'.$copyright.'</span><br>';
264        }
265        $comment.= $details;
266
267        my $form = {
268            method => 'pwg.categories.setInfo',
269            category_id => $params{id},
270            comment => $comment,
271        };
272
273        if (defined $title) {
274            $form->{name} = $title;
275        }
276
277        piwigo_login();
278
279        my $response = $ua->post(
280            $conf{base_url}.'/ws.php?format=json',
281            $form
282        );
283    }
284}
285
286sub set_photo_properties {
287    my %params = @_;
288
289    print '[set_photo_properties] for "'.$params{path}.'"'."\n" if $opt{debug};
290
291    # is there any title defined in a descript.ion file?
292    my $desc_filepath = dirname($params{path}).'/descript.ion';
293
294    if (not -f $desc_filepath) {
295        print '[set_photo_properties] no descript.ion file'."\n" if $opt{debug};
296        return;
297    }
298
299    my $property = undef;
300    my $photo_filename = basename($params{path});
301    open(IN, '<', $desc_filepath);
302    while (my $desc_line = <IN>) {
303        if ($desc_line =~ /^$photo_filename/) {
304            chomp($desc_line);
305            $property = (split /\t/, $desc_line, 2)[1];
306        }
307    }
308    close(IN);
309
310    if (defined $property and $property ne '') {
311        print '[photo '.$params{id}.'] "';
312
313        if (defined $opt{short_lines}) {
314            print basename($params{path});
315        }
316        else {
317            print $params{path};
318        }
319
320        print '", set photo description "'.$property.'"'."\n";
321
322        my $form = {
323            method => 'pwg.images.setInfo',
324            image_id => $params{id},
325            single_value_mode => 'replace',
326            comment => $property,
327        };
328
329        piwigo_login();
330
331        my $response = $ua->post(
332            $conf{base_url}.'/ws.php?format=json',
333            $form
334        );
335    }
336}
337
338sub add_photo {
339    my %params = @_;
340
341    my $form = {
342        method => 'pwg.images.addSimple',
343        image => [$params{path}],
344        category => $params{album_id},
345    };
346
347    print '[album '.$params{album_id}.'] "';
348    if (defined $opt{short_lines}) {
349        print basename($params{path});
350    }
351    else {
352        print $params{path};
353    }
354    print '" upload starts... ';
355
356    $| = 1;
357    my $t1 = [gettimeofday];
358
359    piwigo_login();
360    my $response = $ua->post(
361        $conf{base_url}.'/ws.php?format=json',
362        $form,
363        'Content_Type' => 'form-data',
364    );
365
366    my $photo_id = from_json($response->content)->{result}{image_id};
367
368    my $elapsed = tv_interval($t1);
369    print ' completed ('.sprintf('%u ms', $elapsed * 1000).', photo '.$photo_id.')'."\n";
370
371    return $photo_id;
372}
373
374sub add_to_piwigo {
375    # print $File::Find::name."\n";
376    my $path = $File::Find::name;
377    my $parent_dir = dirname($album_dir);
378    if ($parent_dir ne '.') {
379        # print '$parent_dir = '.$parent_dir."\n";
380        $path =~ s{^$parent_dir/}{};
381    }
382    # print $path."\n";
383
384    if (-d) {
385        my $up_dir = '';
386        my $parent_id = undef;
387
388        if (defined $conf{parent_album_path}) {
389            $up_dir = $conf{parent_album_path}.' / ';
390            $parent_id = $conf{parent_album_id};
391        }
392
393        foreach my $dir (split '/', $path) {
394            my $is_new_album = 0;
395
396            if (not defined $piwigo_albums{$up_dir.$dir}) {
397                my $id = cached_album(dir => $up_dir.$dir);
398                # if the album is not in the cache OR if the id in the cache
399                # matches no album fetched by pwg.categories.getList, then
400                # we have to create the album first
401                if (not defined $id or not grep($_ eq $id, values(%piwigo_albums))) {
402                    print 'album "'.$up_dir.$dir.'" must be created'."\n";
403                    $is_new_album = 1;
404                    $id = add_album(name => $dir, parent => $parent_id);
405                    cache_add_album(dir => $up_dir.$dir, id => $id);
406                }
407                $piwigo_albums{$up_dir.$dir} = $id;
408            }
409
410            if ($is_new_album or defined $opt{reload_properties}) {
411                set_album_properties(dir => $up_dir.$dir, id => $piwigo_albums{$up_dir.$dir});
412            }
413
414            $parent_id = $piwigo_albums{$up_dir.$dir};
415            $up_dir.= $dir.' / ';
416        }
417    }
418
419    if (-f and $path =~ /\.(jpe?g|gif|png)$/i) {
420        my $album_key = join(' / ', split('/', dirname($path)));
421
422        if (defined $conf{parent_album_path}) {
423            $album_key = $conf{parent_album_path}.' / '.$album_key;
424        }
425
426        my $album_id = $piwigo_albums{$album_key};
427
428        my $image_id = photo_exists(album_id => $album_id, file => basename($File::Find::name));
429        if (not defined $image_id or $image_id < 1) {
430            $image_id = cached_photo(path => $File::Find::name, dir => $album_key);
431        }
432
433        if (defined $image_id and $image_id >= 1) {
434            if (not $opt{quiet}) {
435                print $File::Find::name.' already exists in Piwigo, skipped'."\n";
436            }
437
438            if (defined $opt{reload_properties}) {
439                set_photo_properties(path => $File::Find::name, id => $image_id);
440            }
441
442            return 1;
443        }
444
445        $image_id = add_photo(path => $File::Find::name, album_id => $album_id);
446        set_photo_properties(path => $File::Find::name, id => $image_id);
447        cache_add_photo(path => $File::Find::name, dir => $album_key, id => $image_id);
448    }
449}
450
451sub cache_add_photo {
452    my %params = @_;
453
454    if (cached_photo(path => $params{path}, dir => $params{dir})) {
455        if (not $opt{quiet}) {
456            print 'photo is in the cache, no upload'."\n";
457        }
458        return 1;
459    }
460
461    $params{dir} =~ s{ / }{/}g;
462
463    my $filepath = $params{dir}.'/.piwigo_import_tree.txt';
464
465    open(my $ofh, '>> '.$filepath) or die 'cannot open file "'.$filepath.'" for writing';
466    print {$ofh} $conf{base_url}.' '.md5_hex(basename($params{path}));
467
468    if (defined $params{id}) {
469        print {$ofh} ' [id='.$params{id}.']';
470    }
471
472    print {$ofh} "\n";
473    close($ofh);
474}
475
476sub cached_photo {
477    my %params = @_;
478
479    $params{dir} =~ s{ / }{/}g;
480
481    my $filepath = $params{dir}.'/.piwigo_import_tree.txt';
482
483    if (not -f $filepath) {
484        return undef;
485    }
486
487    my $photo_id = undef;
488    my $photo_filename_md5 = md5_hex(basename($params{path}));
489
490    open(my $ifh, '<'.$filepath) or die 'cannot open file "'.$filepath.'" for reading';
491    while (my $line = <$ifh>) {
492        chomp $line;
493        if ($line =~ m/$photo_filename_md5/) {
494            # TODO if needed : search the [id=(\d+)] for photo_id
495            if ($line =~ m/\[id=(\d+)\]/) {
496                return $1;
497            }
498            else {
499                return -1; # true, but not an image_id
500            }
501        }
502    }
503    close($ifh);
504
505    return undef;
506}
507
508sub cache_add_album {
509    my %params = @_;
510
511    $params{dir} =~ s{ / }{/}g;
512
513    my $filepath = $params{dir}.'/.piwigo_import_tree.txt';
514
515    open(my $ofh, '>> '.$filepath) or die 'cannot open file "'.$filepath.'" for writing';
516    print {$ofh} $conf{base_url}.' album_id = '.$params{id}."\n";
517    print $conf{base_url}.' album_id = '.$params{id}."\n";
518    close($ofh);
519}
520
521sub cached_album {
522    my %params = @_;
523
524    $params{dir} =~ s{ / }{/}g;
525
526    my $filepath = $params{dir}.'/.piwigo_import_tree.txt';
527
528    if (not -f $filepath) {
529        return undef;
530    }
531
532    my $album_id = undef;
533
534    open(my $ifh, '<'.$filepath) or die 'cannot open file "'.$filepath.'" for reading';
535    while (my $line = <$ifh>) {
536        chomp $line;
537        if ($line =~ m/album_id = (\d+)/) {
538            $album_id = $1;
539        }
540    }
541    close($ifh);
542
543    print 'directory "'.$params{dir}.'" was found as album '.$album_id."\n";
544
545    return $album_id;
546}
Note: See TracBrowser for help on using the repository browser.