Created
January 15, 2021 09:57
-
-
Save briandfoy/30be6d46c005dc27003518ece2f53a74 to your computer and use it in GitHub Desktop.
Convert an ePub to Mobi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
use v5.26; | |
use experimental qw(signatures); | |
=head1 NAME | |
zamzar_epub_to_mobi - use Zamzar to convert an ePub to Mobi | |
=head1 SYNOPSIS | |
% zamzar_epub_to_mobi file.epub [file2.epub] | |
=head1 DESCRIPTION | |
Turn F<.epub> files into Amazon's F<.mobi> format using the Zamzar API. | |
The program sends a file to Zamzar's service, then polls the service | |
until the status says the file is ready. After that it downloads the | |
file. | |
You can give this multiple files and they will be handled concurrently. | |
This only handles the free "sandbox" level, so if you have a paid account | |
you need to change C<sandbox.zamzar.com> to C<api.zamzar.com> by setting | |
the C<ZAMZAR_HOST> environment variable. | |
=head2 Set your API key | |
Get an API key from L<https://developers.zamzar.com/docs>, then put it | |
in the C<ZAMZAR_API_KEY> environment variable. | |
=head2 Logging | |
Set the log level in the C<ZAMZAR_LOG_LEVEL> environment variable. The valid | |
values are the ones listed in L<Mojo::Log>. By default, the log level is | |
C<info>. | |
=head1 AUTHOR | |
brian d foy, [email protected] | |
=head1 COPYRIGHT | |
Copyright © 2021, brian d foy, All rights reserved. | |
=head1 LICENSE | |
=cut | |
run( @ARGV ) unless caller; | |
sub run ( @args ) { | |
my %job_ids = map { create_jobs($_) => $_ } @args; | |
my( $total_cost, $total_files ) = 0; | |
while( keys %job_ids ) { | |
foreach my $job_id ( keys %job_ids ) { | |
# initializing converting successful failed | |
my( $json ) = check_job( $job_id ); | |
my( $status ) = $json->{status}; | |
mlog()->info( "file <$job_ids{$job_id}> job <$job_id> status <$status>" ); | |
if( $status eq 'failed' ) { | |
my( $code, $message ) = $json->{failure}->@{qw(code message)}; | |
mlog()->error( "file <$job_ids{$job_id}> job <$job_id> failed <$status> code <$code> message <$message>!" ); | |
delete $job_ids{$job_id}; | |
} | |
if( my $file_id = eval{ $json->{target_files}[0]{id} } ) { | |
mlog()->info( "Downloading output file for <$job_ids{$job_id}> job <$job_id> cost: <$json->{credit_cost}>" ); | |
download_file( $file_id ); | |
delete $job_ids{$job_id}; | |
$total_cost += $json->{credit_cost}; | |
$total_files += 1; | |
} | |
} | |
sleep 15; | |
} | |
mlog()->info( "Total files: $total_files Cost: $total_cost" ); | |
my $details = account_details(); | |
mlog()->info( "Test credits remaining: " . $details->{test_credits_remaining} ); | |
mlog()->info( "Credits remaining: " . $details->{credits_remaining} ); | |
exit( ! $total_files == @args ); | |
} | |
sub ua { | |
state $rc = require Mojo::UserAgent; | |
state $rc = require Mojo::Util; | |
state $ua = do { | |
my $api_key = $ENV{'ZAMZAR_API_KEY'}; | |
mlog()->fatal( "ZAMZAR_API_KEY not set!" ) unless $api_key; | |
die "Set ZAMZAR_API_KEY to make requests!" unless $api_key; | |
my $armored = Mojo::Util::b64_encode( join( ':', $api_key, ''), '' ); | |
my $ua = Mojo::UserAgent->new; | |
$ua->on( | |
prepare => sub ( $ua, $tx ) { | |
$tx->req->headers->header( "Authorization" => "Basic $armored" ); | |
} | |
); | |
$ua; | |
}; | |
$ua | |
} | |
sub mlog { | |
state $rc = require Mojo::Log; | |
state $log = do { Mojo::Log->new( level => $ENV{ZAMZAR_LOG_LEVEL} // 'info' ) }; | |
$log; | |
} | |
sub get_url ( $endpoint ) { | |
state $host = do { | |
if( $ENV{ZAMZAR_HOST} ) { $ENV{ZAMZAR_HOST} } | |
elsif( is_paid_account() ) { 'api.zamzar.com' } | |
else { 'sandbox.zamzar.com' } | |
}; | |
state $rc = do { | |
mlog()->debug( "ZAMZAR_HOST is <$ENV{ZAMZAR_HOST}>" ); | |
mlog()->debug( "API host is <$host>" ); | |
}; | |
state $base = Mojo::URL->new( "https://$host/" ); | |
$base->clone->path( $endpoint ); | |
} | |
sub create_jobs ( @files ) { | |
my @job_ids; | |
FILE: foreach my $file ( @files ) { | |
unless( -e $file ) { | |
mlog()->error( "File <$file> does not exist! Skipping." ); | |
next; | |
} | |
my $tx = ua()->post( | |
get_url( "v1/jobs" ), | |
form => { | |
source_file => { file => $file }, | |
target_format => 'mobi' | |
} | |
); | |
unless( $tx->result->is_success ) { | |
# {"errors":[{"code":21,"message":"resource does not exist"}]} | |
my( $code, $message ) = eval { $tx->result->json->@{ qw(code message) } }; | |
mlog()->error( "Job for file <$file> failed! Code <$code> message <$message>." ); | |
mlog()->error( $tx->req->headers->to_string ); | |
mlog()->error( $tx->result->body ); | |
next FILE; | |
} | |
push @job_ids, $tx->result->json->{id}; | |
mlog()->info( "Created job $job_ids[-1]" ); | |
} | |
@job_ids; | |
} | |
=pod | |
{ | |
"id" : 15, | |
"key" : "...", | |
"status" : "successful", | |
"sandbox" : true, | |
"created_at" : "2013-10-27T13:41:00Z", | |
"finished_at" : "2013-10-27T13:41:13Z", | |
"source_file" : {"id":2,"name":"portrait.gif","size":90571}, | |
"target_files" : [{"id":3,"name":"portrait.png","size":15311}], | |
"target_format" : "png", | |
"credit_cost" : 1 | |
} | |
=cut | |
sub check_job ( $job_id ) { | |
my $url = get_url( "/v1/jobs/$job_id" ); | |
mlog()->debug( "download_file: URL is $url" ); | |
mlog()->info( "Checking job id <$job_id>" ); | |
my $json = ua()->get( $url )->result->json; | |
} | |
sub download_file ( $file_id ) { | |
my $url = get_url( "/v1/files/$file_id/content" ); | |
mlog()->debug( "download_file: URL is $url" ); | |
my $result = ua()->get( $url )->result; | |
my $disposition = $result->headers->header( 'Content-Disposition' ); | |
my( $filename ) = $disposition =~ /filename="(.+)"/; | |
mlog()->error( "There's no filename in <$disposition>" ) unless $filename; | |
mlog()->info( "filename is <$filename>" ); | |
$result->save_to( $filename ); | |
unless( -e $filename ) { | |
mlog()->error( "Filename was not saved. WTF?" ); | |
} | |
} | |
=pod | |
{ | |
"test_credits_remaining" : 3, | |
"credits_remaining" : 18, | |
"plan" : { | |
"name" : "Developer", | |
"price_per_month" : 0, | |
"conversions_per_month" : 25, | |
"maximum_file_size" : 1048576 | |
} | |
} | |
=cut | |
sub account_details { | |
# This can't use the get_url because the get_url uses this | |
# to figure out if it's a paid account. | |
my $url = "https://sandbox.zamzar.com/v1/account"; | |
mlog()->debug( "download_file: URL is $url" ); | |
ua()->get( $url )->result->json; | |
} | |
sub is_paid_account { | |
account_details()->{plan}{price_per_month} != 0; | |
} | |
1; | |
__END__ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment