The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#! /usr/bin/perl
#---------------------------------------------------------------------
# install-audiobook.pl
# Created by Christopher J. Madsen
#
# This example script is in the public domain.
#
# Copy an audiobook to your player
#---------------------------------------------------------------------

use strict;
use warnings;
use 5.010;

use Image::Size qw(imgsize); # Comment this out if you don't have cover images
use Path::Class qw(dir);
use Lingua::Conjunction qw(conjunction);
Lingua::Conjunction->connector('&');

use Media::LibMTP::API qw(Get_First_Device LIBMTP_FILETYPE_JPEG
                          LIBMTP_FILETYPE_OGG);

# This script is specific to the way I handle my audiobooks, but you
# may be able to adapt it to your needs.  I have a basic M3U playlist
# named book.m3u, which lists the tracks in OGG format.
#
# On my device, books are stored under the Audiobooks top-level folder,
# first in a folder by author, then by title.

my $audiobook_folder_name = 'Audiobooks';
my $playlist_filename     = 'book.m3u';
my $cover_filename        = 'cover.jpg';

#---------------------------------------------------------------------
# First, we open the MTP device.  For some reason, this tends to fail
# on the first try, but will usually succeed if you keep trying.

my $device;

while (1) {
  $device = Get_First_Device() and last;
  say STDERR "Trying again in 5 seconds...";
  sleep 5;
}

#=====================================================================
# Routines to deal with finding/creating folders:
#---------------------------------------------------------------------
# Find an existing child folder by name:
#
# Input:
#   folder: the parent Folder object
#   name:   the folder name we're looking for
#
# Returns:
#   The desired Folder object, or undef if not found
#
# Notes:
#   Only the immediate children of folder are searched

sub find_folder
{
  my ($folder, $name) = @_;

  while ($folder and $folder->name ne $name) {
    $folder = $folder->sibling;
  }

  return $folder;
} # end find_folder

#---------------------------------------------------------------------
# Create a new folder:
#
# Input:
#   name:       the new folder name
#   parent_id:  the folder ID of the parent folder
#   storage_id: the storage ID of the parent folder
#
# Returns:
#   The folder ID of the new folder

sub create_folder
{
  my ($name, $parent_id, $storage_id) = @_;

  my $new_id = $device->Create_Folder(
    $name, $parent_id, $storage_id
  ) or die "Creating folder $name failed: " . $device->errstr;

  return $new_id;
} # end create_folder

#---------------------------------------------------------------------
# Create a new folder to store the audiobook:
#
# Input:
#   author: the author's name (1st level folder)
#   title:  the book's title (2nd level folder)
#
# Returns:
#   The folder ID of the new folder
#
# Notes:
#   Dies if the title folder already exists.

sub create_book_folder
{
  my ($author, $title) = @_;

  my $folderList = $device->Get_Folder_List;

  my $audiobook_folder = find_folder($folderList, $audiobook_folder_name)
      or die "Can't find $audiobook_folder_name folder";
  my $storage_id = $audiobook_folder->storage_id;

  my ($author_folder);
  if (defined $author) {
    $author_folder = find_folder($audiobook_folder->child, $author)
        // create_folder($author, $audiobook_folder->folder_id, $storage_id);
  } else {
    $author_folder = $audiobook_folder;
  }

  if (ref $author_folder) {
    if (my $f = find_folder($author_folder->child, $title)) {
      #return $f->folder_id;     # FIXME
      printf STDERR "%s already exists in %s, skipping it\n",
          $title, $author_folder->name;
      return undef;
    }
    $author_folder = $author_folder->folder_id;
  }

  return create_folder($title, $author_folder, $storage_id);
} # end create_book_folder

#=====================================================================
# Main loop:
#---------------------------------------------------------------------
BOOK:
for my $directory (@ARGV) {
  # Make sure we got a directory:
  die "Usage: $0 DIRECTORY...\n" unless -d $directory;

  say "\n$directory...";

  $directory = dir($directory);

  # Read the playlist:
  my $playlist = $directory->file($playlist_filename);
  die "$playlist does not exist" unless -e $playlist;

  my @filenames = $playlist->slurp(chomp => 1, iomode => '<:utf8:crlf');

  die "$playlist is empty\n" unless @filenames;

  # Make sure we aren't missing any files:
  for my $fn (@filenames) {
    die "unrecognized line in $playlist: $fn\n" unless $fn =~ /\.ogg\z/i;
    die "$fn does not exist\n" unless -e $directory->file($fn);
  }

  # Copy each track to the device:
  my ($artist, $album_name, $folder_id, @tracks);
  for my $tracknumber (0 .. $#filenames) {
    my $title;
    my $fn = $directory->file($filenames[$tracknumber]);
    # Read track comments:
    {
      my @artists;
      open(my $in, '-|:utf8', qw(vorbiscomment --list), $fn)
          or die "vorbiscomment failed on $fn: $!";
      while (<$in>) {
        if (/^title=(.+)/i) { $title = $1 }
        elsif ($tracknumber > 0) { } # only get artist & album from first track
        elsif (/^artist=(.+)/i) { push @artists, $1 }
        elsif (/^album=(.+)/i)  { $album_name = $1 }
      }
      close $in;

      $artist = conjunction(@artists) if @artists;

      die "No chapter title" unless defined $title;

      if ($tracknumber == 0) {
        die "No book title\n" unless defined $album_name;

        say "Album:  $album_name";
        say "Artist: $artist" if defined $artist;

        $folder_id = create_book_folder($artists[0], $album_name)
            or next BOOK;
      } # end if track 0
    } # end reading comments from track

    # Load the Track object with metadata for the new track:
    my $stat = $fn->stat;

    my $track = Media::LibMTP::API::Track->new;
    $track->parent_id($folder_id);
    $track->tracknumber($tracknumber);
    $track->title($title);
    $track->artist($artist) if defined $artist;
    $track->album($album_name);
    $track->filename($filenames[$tracknumber]);
    $track->filesize( $stat->size );
    $track->modificationdate( $stat->mtime );
    $track->filetype(LIBMTP_FILETYPE_OGG);

    # Send the track to the device:
    say "Sending $filenames[$tracknumber]...";
    $device->Send_Track_From_File("$fn", $track)
        and die "Sending $fn failed: " . $device->errstr;

    push @tracks, $track->item_id;
  } # end for $tracknumber in @filenames

  # Now create the Album for the book:
  my $album = Media::LibMTP::API::Album->new;

  $album->parent_id($folder_id);
  $album->name($album_name);
  $album->artist($artist) if defined $artist;
  $album->tracks(\@tracks);

  $device->Create_New_Album($album)
      and die "Create_New_Album failed: " . $device->errstr;

  # Report details about the new album:
  my $album_id   = $album->album_id;

  say "Title:    " . $album->name . " ($album_id)";
  say "Author:   " . ($album->artist // 'Unknown Author');
  say "Parent:   " . $album->parent_id;
  say "Storage:  " . $album->storage_id;
  say "Tracks: " . join(', ', @{$album->tracks})
      . ' (' . $album->no_tracks . ')';

  # Now transfer the cover image, if available:
  my $cover = $directory->file($cover_filename);

  if (-e $cover) {
    say "Sending $cover_filename...";

    my ($width, $height) = imgsize("$cover");

    my $data = $cover->slurp(iomode => '<:raw');

    my $sample = Media::LibMTP::API::FileSampleData->new;
    $sample->width($width);
    $sample->height($height);
    $sample->filetype(LIBMTP_FILETYPE_JPEG);
    $sample->data($data);

    $device->Send_Representative_Sample($album_id, $sample)
        and warn "Sending $cover failed\n";
  } # end if cover.jpg exists
} # end while @ARGV

undef $device;