Dotfiles, utilities, and other apparatus.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
4.7 KiB

  1. #!/usr/bin/env perl
  2. =pod
  3. =head1 NAME
  4. digikam-extract - find files by tag in a DigiKam 4 database
  5. =head1 SYNOPSIS
  6. # List photos tagged "mom-album-2020":
  7. digikam-extract --tag mom-album-2020
  8. # Copy tagged items to current dir, renamed by date:
  9. digikam-extract --tag mom-album-2020 --copy
  10. # Same, but falling back to file mtime for anything with a year of 1980:
  11. digikam-extract --tag mom-album-2020 --ctime-fallback 1980 --copy
  12. =head1 DESCRIPTION
  13. A silly hack for tagging photos in DigiKam and then copying the tagged stuff
  14. into a separate directory and renaming the files according to their creation
  15. date. If I need this workflow again, I may generalize this, but it's probably
  16. worth noting that the database schema is effectively an undocumented API and
  17. could easily get yanked out from under this code at any time.
  18. =head1 OPTIONS
  19. =over 4
  20. =item B<--tag> [tagname]
  21. Specify a tag to search for. Required.
  22. =item B<--copy> [path]
  23. Copy files to destination path. Defaults to working directory.
  24. =item B<--dest> [path]
  25. Specify a destination path.
  26. =item B<--ctime-fallback> [year]
  27. Specify a year which probably means that the camera's date wasn't set, and fall
  28. back to whatever DigiKam's db thinks the modification time is instead.
  29. =item B<--dbfile> [path]
  30. Specify location of DigiKam4 database file.
  31. =item B<--date-format> "format"
  32. Specify date format for target filenames. See strftime(3) for date format.
  33. Defaults to C<%Y-%m-%d-%H%M%S>.
  34. =item B<--root> [path]
  35. Specify a gallery path. Must correspond to a root album folder defined in
  36. Collections Settings in DigiKam. No trailing slash.
  37. =item B<--help>
  38. Print help and exit.
  39. =back
  40. =head1 AUTHOR
  41. Brennen Bearnes
  42. =cut
  43. use warnings;
  44. use strict;
  45. use Cwd;
  46. use DBI;
  47. use Data::Dumper;
  48. use File::Basename;
  49. use File::Spec;
  50. use Getopt::Long;
  51. use Pod::Usage;
  52. use SQL::Abstract;
  53. use Time::Piece;
  54. use Time::Seconds;
  55. use File::Copy;
  56. use File::HomeDir;
  57. my $HOME = File::HomeDir->my_home;
  58. # Handle options, including help generated from the POD above.
  59. # TODO: This should use digikam config to extract current path to db
  60. # and roots should be extracted from the database, most likely.
  61. my $copy = 0;
  62. my $tag;
  63. my $dest = getcwd();
  64. my $ctime_fallback;
  65. my $date_format = '%Y-%m-%d-%H%M%S';
  66. my $dbfile = $HOME . "/digikam-db/digikam4.db";
  67. my $root = $HOME . '/workspace/photos';
  68. GetOptions(
  69. 'copy' => \$copy,
  70. 'dest=s' => \$dest,
  71. 'tag=s' => \$tag,
  72. 'root=s' => \$root,
  73. 'dbfile=s' => \$dbfile,
  74. 'ctime-fallback=s' => \$ctime_fallback,
  75. 'date-format' => \$date_format,
  76. 'help' => sub { pod2usage(-verbose => 2) },
  77. ) or pod2usage(-verbose => 1, );
  78. # Handle a couple of likely failure modes:
  79. unless (defined $tag) {
  80. print "Please specify a tag name with --tag\n";
  81. exit(2);
  82. }
  83. foreach my $path ($dest, $root) {
  84. die("$path is not a directory")
  85. unless -d $path;
  86. }
  87. my $query = q{
  88. SELECT i.id, ii.creationDate, i.modificationDate, ar.specificPath,
  89. a.relativePath, i.name AS image_name, t.name AS tag_name
  90. FROM Tags t JOIN ImageTags it ON it.tagid = t.id
  91. JOIN Images i ON it.imageid = i.id
  92. JOIN Albums a ON i.album = a.id
  93. JOIN AlbumRoots ar ON ar.id = a.albumRoot
  94. JOIN ImageInformation ii ON ii.imageid = i.id
  95. WHERE
  96. t.name = ? AND ar.specificPath = ?
  97. ;
  98. };
  99. my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile", "", "");
  100. my @bind = ($tag, $root);
  101. my $sth = $dbh->prepare($query);
  102. $sth->execute(@bind);
  103. unless ($copy) {
  104. print "Dry run. Would copy:\n\n"
  105. }
  106. my %sources;
  107. while (my $data = $sth->fetchrow_hashref()) {
  108. my $cdate = Time::Piece->strptime($data->{creationDate}, "%Y-%m-%dT%T");
  109. my $mdate = Time::Piece->strptime($data->{modificationDate}, "%Y-%m-%dT%T");
  110. my $date = $cdate->strftime($date_format);
  111. if (defined $ctime_fallback) {
  112. # If we got a year to fall back to the mtime for, then do that -
  113. # so like if a bunch of stuff has 1980 because the clock wasn't
  114. # set right on the camera...
  115. if ($ctime_fallback eq $cdate->strftime('%Y')) {
  116. $date = $mdate->strftime($date_format);
  117. }
  118. }
  119. # print "$date\t";
  120. my $source = $root . $data->{'relativePath'} . "/" . $data->{image_name};
  121. my $dest = "$dest/$date-$data->{image_name}";
  122. if (defined $sources{$dest}) {
  123. die("Collision: $source and $sources{$dest} would collide, making no copies");
  124. } else {
  125. $sources{$dest} = $source;
  126. }
  127. }
  128. foreach my $dest (keys %sources) {
  129. my $source = $sources{$dest};
  130. print "$source -> $dest\n";
  131. next unless $copy;
  132. if (-e $dest) {
  133. print "Destination already exists: $dest\n";
  134. } else {
  135. copy($source, $dest);
  136. }
  137. }
  138. unless ($copy) {
  139. print "\nAdd --copy to invocation to copy to $dest\n";
  140. }