Perl - YouTube の動画の音声を iTunes に登録

Perl のモジュール WWW-YouTube-Downloadffmpeg を使って、YouTube から動画をダウンロードしてきてその音声を iTunes に登録させる話。LWP::UserAgent のコールバックの書き方が勉強になった。

  • Windows でも Mac でも OK
  • ffmpeg は Windows は お気に入りの動画を携帯で見よう から、Mac なら Homebrew からインストールできる。 PATH を通しておく。
  • 作業するディレクトリは Windows はテンプフォルダ、Mac はダウンロードフォルダにしてある。ソースをいじって適当に変更した方がいいかも。Windows の場合は後で start コマンドを使うためフルパスで記述すべし。
  • ダウンロードしてくる動画はビットレートの高い音声 aac な動画にしたつもり。Wikipedia を参考に手動で @aac_fmt_list をいじってもいい。
  • iTunes は設定で「ライブラリへの追加時にファイルを"iTunes Media"フォルダにコピーする」のチェックをしておく。
  • 動画のダウンロードだけなら WWW::YouTube::Download モジュール同封の youtube-download コマンドで簡単にできる。
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode;
use WWW::YouTube::Download;
use Term::ReadKey;
use Errno ();
use List::Util qw(first);
use Cwd;
use IO::File;

# ダウンロードしたい音声 aac な fmt の優先順位
my @aac_fmt_list = (37,35,34,18,22);

my ($terminal_width) = GetTerminalSize();
my $encoder = $^O eq "MSWin32" ? find_encoding("cp932")
            : $^O eq "darwin"  ? find_encoding("utf8")
            :                    die "Only for Mac or Windows.";

STDOUT->autoflush;

# 作業するディレクトリの指定
my $root_dir = $ENV{TEMP} || $ENV{TMP} || "$ENV{HOME}/Downloads";
#my $root_dir = Cwd::getcwd;

chdir $root_dir;
my $workspace = 'www-youtube-download';
unless (mkdir $workspace or $! == Errno::EEXIST) {
    die "Failed to create dir:$workspace:$!";
}
chdir $workspace;

print "Input a video URL:\n";
chomp(my $video_url = <STDIN>);

my $client = WWW::YouTube::Download->new;

# title, fmt, suffix を決定
my $meta_data = $client->prepare_download($video_url);
my $title = decode_utf8($meta_data->{title});
$title = filename_normalize($title);
$title = $encoder->encode($title, sub {sprintf 'U+%x', shift});
my $fmt = get_aac_fmt($meta_data->{fmt_list})
    or die "Error: The audio codec of the video is not aac.\n";
my $suffix = $meta_data->{video_url_map}{$fmt}{suffix};

# ビデオダウンロード
open my $wfh, '>', "$title.$suffix"
    or die "Error: Can't create a file: $!";
binmode $wfh;
print "Downloading: $title ($suffix, $fmt)\n";
eval {
    $client->download($video_url, {
        fmt => $fmt,
        cb  => \&callback,
    });
};
die "Error: Can't complete downloading: $@" if $@;
close $wfh;
print "Successfully finished downloading.\n";

# ffmpeg
my $nul = $^O eq "MSWin32" ? 'NUL' : '/dev/null';
system qq|ffmpeg -i "$title.$suffix" -vn |
      .qq|-acodec copy -y "$title.m4a" 1> $nul 2>&1|;

# iTunes
if ($^O eq 'MSWin32') {
    system qw|start iTunes|, "$root_dir/$workspace/$title.m4a";
}
else {
    system qw{open -a /Applications/iTunes.app}, "$title.m4a";
}

# unlink files
print "Please wait for a while to unlink $suffix and m4a files...";
sleep 8;
if (unlink "$title.$suffix", "$title.m4a") {
    print " OK\n";
}
else {
    warn "\nFailed to unlink files: $!\n";
}


sub callback {
    my ($chunk, $res, $proto) = @_;
    print {$wfh} $chunk;

    my $size = tell $wfh;
    my $total = $res->header('Content-Length');
    my $width = $terminal_width - 26;
    my $progress = int ($size / $total * $width);
    print 
        sprintf(
            "Total:%5.1fMB (%5.1f%%)",
            $total / 1024 / 1024, $size / $total * 100
        ),
        " [", "=" x $progress, " " x ($width - $progress), "]\r";
    print "\n" if $total == $size;
}

sub get_aac_fmt {
    my %get_fmt_list = map { $_ => 1 } @{shift()};
    first { exists $get_fmt_list{$_} } @aac_fmt_list;
}

# 以下は youtube-download からの丸写し
sub filename_normalize {
    my $filename = shift;
    $filename =~ s#[[:cntrl:]]##smg;          # remove all control characters
    $filename =~ s#^\s+|\s+$##g;              # trim spaces
    $filename =~ s#^\.+##;                    # remove multiple leading dots
    $filename =~ tr#"/\\:*?<>|#'\-\-\-_____#; # NTFS and FAT unsupported characters
    return $filename;
}

以上を適当な名前で保存して実行してみるとこんな感じ。
f:id:ks0608:20120126005915p:plain

今日の疑問

  • system iTunes コマンドの引数をフルパスで指定しなければいけないの?
  • サブルーチン get_aac_fmt の作り方を工夫したが、これでいいのか?
  • コールバックの中で画面幅分 - 1 のプログレスバーを作ったけど、これは画面幅丁度だと Windows でうまくいかなかったから。なんで?
  • 後処理で www-youtube-download フォルダも消そうとしたけど、消せないことが多々起きたのでそのまま残すことにした。なぜ消せない?
  • iTunes を呼び出した後にすぐファイルを消すと iTunes によってコピーされる前に消してしまうことが起きた。だから sleep 8; をして待ってから消してるんだけど、もっといい方法はあるか?