#! /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;