#!/usr/bin/perl -CS -w # This program is free software. It comes without any warranty, to the # extent permitted by applicable law. You can redistribute it and/or # modify it under the terms of the Do What The Fuck You Want To Public # License, Version 2, as published by Sam Hocevar. # See http://sam.zoy.org/wtfpl/COPYING for more details. $VERSION = "0.1, 17 February 2012"; use Getopt::Long qw/:config noauto_abbrev nogetopt_compat no_ignore_case permute bundling auto_version auto_help/; use Pod::Usage; use IMDB::Film; use Locale::Language qw /code2language LOCALE_LANG_ALPHA_2 LOCALE_LANG_ALPHA_3/; use DBI; use IPC::Open3; use File::Basename qw /basename/; use File::Spec::Functions; use File::Copy qw /move/; use Env qw /HOME/; use Switch qw /Perl6/; use utf8; use strict; ################################################################################ # Configuration my $confile = catfile ($HOME, '.videodb.rc'); die "Error: Can't read `" .$confile. "'.\n" unless -f $confile; my %config = do $confile; die "Error in `" .$confile. "'.\n" if $@ || not %config; map { exists $config{$_} || die "Error: Missing `${_}'.\n" } qw /videodir driver database hostname user port password userid imdb url/; $config{prefix} = "" unless exists $config{prefix}; my $symlinks = catdir($config{videodir},'MOVIES'); # Symlinks folder die "Error: No such directory: `" .$symlinks. "'.\n" unless -d $symlinks; my $directors = catdir($config{videodir},'DIRECTORS'); # Directors folder die "Error: No such directory: `" .$directors. "'.\n" unless -d $symlinks; my %imdb = ( host => $config{imdb}, debug => 0 ); $config{url} =~ s/\/*$//; ################################################################################ =head1 NAME videoadd.pl - add movies to your collection =head1 SYNOPSIS B [B<-s>] [B<--ignore-db>] [B<--dont-sort>] [B<-i> I] [B<-o> I...] [B<-q>] I =head1 DISCLAIMER Your collection is assumed to have the following structure: two folders, I and I, that have the same parent. I contains one subdirectory for each director, and each movie lies (B) in the subdirectory of its director. The folder I contains symlinks - one for each movie - that target to I<../DIRECTORS/director_of_the_movie/movie>. The behavior of B is NOT specified if your collection does not verify these conventions! |- ... |- DIRECTORS | |- director1 | |- |- movie11 | | `- movie12 | |- director2/ | `- ... `- MOVIES |- movie11 -> ../DIRECTORS/director1/movie1 |- movie12 -> ../DIRECTORS/director1/movie2 `- ... =head1 DESCRIPTION B add the given movie to your collection. Various data such as the movie length, the A/V codecs, etc. are read from the metadata of the movie. One can fetch other data (e.g., year, director, cast, etc.) from IMDB (option B<-i>). If there is a clash (e.g., language) the metadata take precedence. The database is then updated with the new movie (unless B<--ignore-db> is set), which in turn is put in the right place of the above hierarchy (unless B<--dont-sort> is set). =head1 OPTIONS =over 8 =item B<-s>, B<--seen> Mark the movie as seen for the I of the configuration file. =item B<--ignore-db> Do not connect to the database, hence do not update it with the new movie. Note that it may break the sanity of your collection. =item B<--dont-sort> Do not put the new movie in I, with a symlink I -> I. Note that it may break the sanity of your collection. =item B<-i> I, B<--imdb=>I Search for the given criterion (e.g., a movie title or an IMDB ID) on IMDB. The retrieved data (such as year, director, cast, etc.) will be added to the database (unless overriden by B<-o> I). =item B<-o> I Override data with the I provided. I is one of I, I<language >, I<imdbid>, I<year>, I<imgurl>, I<director>, I<actors>, I<country>, I<plot>, I<rating>, I<istv>. =item B<-q>, B<--quiet> Do not print the final URL for the freshly added movie. =back =head1 CONFIGURATION B<videodb-check> reads its database configuration from the file I<$HOME/.videodb.rc>. This file has to be the content of a Perl hash e.g., videodir => catdir($HOME,'video'), driver => 'mysql', database => 'videodb', hostname => '127.0.0.1', user => 'username', port => 3306, password => '******', prefix => "videodb_", userid => 1 imdb => 'akas.imdb.com', url => 'https://videodb.example.org', =head1 REQUIREMENTS The imported modules are available on CPAN. See the source for details. =head1 AUTHOR Copyright 2012 Guilhem Moulin. See the source for copying conditions. =cut ################################################################################ my $ignoredb_flag; my $seen_flag; my $sort_flag = 1; my %options; GetOptions( "s|seen" => \$seen_flag , "ignore-db" => \$ignoredb_flag , "dont-sort" => sub { undef $sort_flag } , "i|imdb=s" => sub { $imdb{crit} = $_[1] } , "o=s" => sub { my ($k,$v) = split /=/, $_[1], 2; $options{lc $k} = $v; } , "q|quiet=s" => sub { open LOG, '>', '/dev/null' or die "Cannot open `/dev/null': $!" } , "man" => sub { pod2usage(-exitstatus => 0, -verbose => 2) } ) or pod2usage(2); pod2usage(2) if $#ARGV != 0; *LOG = *STDERR unless defined (fileno LOG); my $file = $ARGV[0]; my %new = ( owner_id => $config{userid} , customs => {}); ################################################################################ # Connection to the database my $dbh; unless ( defined $ignoredb_flag ) { # Connect to database my $dsn = "DBI:$config{driver}:database=$config{database};host=$config{hostname};port=$config{port}"; if (exists $config{dbi_misc}) { while (my ($k,$v) = each $config{dbi_misc}) { $dsn .= ";$k=$v"; } } $dbh = DBI->connect($dsn, $config{user}, $config{password}) or die "Error: Can't connect do database.\n"; $dbh->do( "SET NAMES UTF8" ) or die "Error: Can't set names to UTF-8.\n"; } ################################################################################ # Look on-line for information on the movie if (defined $imdb{crit}) { # Look up the title/ID on IMDB my $single; my $movie; do { $movie = new IMDB::Film(%imdb); die "Error: " .$movie->error. "\n" unless $movie->status; my @matches = @{$movie->matched}; if (@matches) { # Got several results; Print them, and ask the user to pick up an ID die "No result found.\n" unless defined $matches[0]->{id}; $imdb{crit} = $matches[0]->{id}; foreach ( @matches ) { print $_->{id}. ' - ' .$_->{title}. "\n" or die "Can't print: $!"; } for (my $i=0; $i<72; $i++) {print '=' or die "Can't print: $!"}; print "\n" or die "Can't print: $!"; print "Choose an ID above: [$matches[0]->{id}] " or die "Can't print: $!"; if ( <STDIN> =~ /(.+)/ ) { $imdb{crit} = $1 }; } else { $single = 1; } } until defined $single; # Got a single result; Process the movie $new{title} = $movie->title(); $new{language} = lc join (', ', @{$movie->language()}); $new{imdbid} = $movie->id(); $new{year} = $movie->year(); $new{imgurl} = $movie->cover(); $new{director} = join ', ', map {$_->{name}} @{$movie->directors()}; $new{actors} = join "\n", map {&mkcast($_)} @{$movie->full_cast()}; $new{country} = join ', ', @{$movie->country()}; $new{plot} = $movie->storyline(); # TODO; plot vs. full_plot vs. storyline? $new{genres} = $movie->genres(); $new{rating} = $movie->rating(); # Ignoring #votes and awards $new{istv} = 1 if $movie->kind() =~ /tv/; # Convert the retrieved data to UTF-8 map { utf8::encode($new{$_}) if exists ($new{$_}) and defined ($new{$_}) and $new{$_} ne "" and not utf8::is_utf8($new{$_}) } qw/title subtitle language diskid comment disklabel imgurl director actors country filename plot/; map { utf8::encode($_) unless utf8::is_utf8($_) } @{$new{genres}}; } elsif (defined $file) { # Fill in at least the title, based on the file name... $new{title} = basename ($file); $new{title} =~ s/.(avi|ogm|ogg|bin|mpe?g|ra?m|mov|asf|wmv|mkv)$//i; } # Override imdDB's information with the ones provided foreach (keys %options) { $new{$_} = $options{$_}; } ################################################################################ # Run mplayer on the given file to get A/V codecs, etc. if ( defined($file) ) { $new{filename} = basename ($file); $new{filesize} = (stat $file)[7]; $new{filedate} = (stat $file)[9]; # Last modify time in seconds since epoch my @cmd = ('mplayer', '-identify', '-ao', 'null', '-vo', 'null', '-frames', '0', $file); open NULL, '+<', '/dev/null' or die "Can't open `/dev/null': $!"; open3 '<&NULL', my $OUT, ">&NULL", @cmd or die "Can't run mplayer"; my (@alang, @slang); foreach my $line (<$OUT>) { next unless $line =~ m/^ID_/; chomp $line; if ( $line =~ m/^ID_VIDEO_FORMAT=(.*)/ ) { $new{video_codec} = &video_codec($1); } elsif ( $line =~ m/^ID_AUDIO_FORMAT=(.*)/ ) { $new{audio_codec} = &audio_codec($1); } elsif ( $line =~ m/^ID_VIDEO_WIDTH=(.*)/ ) { $new{video_width} = $1; } elsif ( $line =~ m/^ID_VIDEO_HEIGHT=(.*)/ ) { $new{video_height} = $1; } elsif ( $line =~ m/^ID_LENGTH=(.*)/ ) { $new{runtime} = $1; die "I won't mess up the db with your crappy empty movie.\n" if $new{runtime} =~ /^0*\.?0*$/; $new{runtime} = sprintf "%d", $new{runtime}/60; } elsif ( $line =~ m/^ID_AID_\d+_LANG=(.*)/ ) { push @alang, &code2lang ($1); } elsif ( $line =~ m/^ID_SID_\d+_LANG=(.*)/ ) { push @slang, &code2lang ($1); } }; $new{language} = lc join (', ', @alang) unless $#alang < 0 and defined $imdb{crit}; # The "Subtitles" field is not hard-coded: insert it in the "custom" field instead $new{customs}->{subtitles} = lc join (', ', @slang); } ################################################################################ # Insertion into the database unless ( defined $ignoredb_flag ) { # Handle custom fields my $customs = $dbh->selectall_arrayref ( "SELECT opt,value FROM $config{prefix}config WHERE opt REGEXP '^custom[1-9]+\$'" ) or die "Can't select: $!\n"; foreach my $c (@$customs) { if (grep {lc $_ eq lc $c->[1]} (keys %{$new{customs}})) { $new{$c->[0]} = $new{customs}->{lc $c->[1]}; } } delete $new{customs}; # Get the (first found) mediatype for files on the hard disk my $mediatype = $dbh->selectrow_arrayref( "SELECT id FROM $config{prefix}mediatypes WHERE name = ?", undef, 'HDD' ) or die "Can't select: $!\n"; # Get the genre IDs my @genres; if (@{$new{genres}}) { my $sth_selgenre = $dbh->prepare( "SELECT id FROM $config{prefix}genres WHERE name = ?" ) or die "Error: " .$dbh->errstr; my $sth_insgenre = $dbh->prepare( "INSERT INTO $config{prefix}genres SET name = ?" ) or die "Error: " .$dbh->errstr; foreach my $g (@{$new{genres}}) { $sth_selgenre->execute ($g); my @gids = $sth_selgenre->fetchrow_array; die $sth_selgenre->errstr if $sth_selgenre->err; unless (@gids) { # Insert non existing genres $sth_insgenre->execute ($g); die $sth_insgenre->errstr if $sth_insgenre->err; $sth_selgenre->execute ($g); @gids = $sth_selgenre->fetchrow_array; die $sth_selgenre->errstr if $sth_selgenre->err; } push @genres, @gids; } $sth_selgenre->finish; $sth_insgenre->finish; } delete $new{genres}; my $INSERT = "INSERT INTO $config{prefix}videodata SET created = NOW()"; if (defined $new{filedate}) { $INSERT .= ", filedate = FROM_UNIXTIME($new{filedate})"; delete $new{filedate}; } $INSERT .= ", mediatype = $mediatype->[0]" if defined ($mediatype) and defined ($mediatype->[0]); while (my ($k,$v) = each %new) { $INSERT .= ", " .$k. " = " .$dbh->quote ($v) if defined $v; } $dbh->do($INSERT) or die "Can't insert: $!\n"; my $ids = $dbh->selectall_arrayref ( "SELECT id FROM $config{prefix}videodata WHERE filename = ? ", undef, $new{filename} ) or die "Can't select: $!\n"; # Retrive the freshly added ID, and update the userseen & videogenre # tables if ($#$ids == 0) { my $id = $ids->[0]->[0]; if ($seen_flag) { $dbh->do( "INSERT INTO $config{prefix}userseen SET video_id = $id, user_id = $config{userid}" ) or die "Can't insert: $!\n"; } my $sth_insgenre = $dbh->prepare( "INSERT INTO $config{prefix}videogenre SET video_id = $id, genre_id = ?" ) or die "Error: " .$dbh->errstr; foreach (@genres) { $sth_insgenre->execute ($_); die $sth_insgenre->errstr if $sth_insgenre->err; } $sth_insgenre->finish; print LOG "Check it out! ", $config{url}. "/show.php?id=" .$id, "\n" or die "Can't print: $!"; } else { warn "Warning: Something weird happened during the INSERT. You should probably check the sanity of your collection.\n"; } $dbh->disconnect; } ################################################################################ # Sort the file if ( defined ($file) and defined ($sort_flag) ) { if (defined $new{director}) { my $dir = catdir ( $directors, $new{director} ); unless (-d $dir) { print STDERR "Directory `$dir' does not exist. "; until (-d $dir) { print STDERR "Should I create it? (Y/n) "; my $a = lc <STDIN>; chomp $a; if ($a eq 'y' or $a eq '') { mkdir $dir or die "Error: Cannot mkdir `$dir': $!\n"; } elsif ($a eq 'n') { exit 0; } } } move ( $file, catfile ( $dir, $new{filename} ) ) or warn "Warning: Cannot move file: $!.\n"; symlink catfile( updir(), 'DIRECTORS', $new{director}, $new{filename} ), catfile( $symlinks, $new{filename} ) or warn "Warning: Cannot create symlink: $!.\n"; } else { warn "Warning: Cannot move file (no director found).\n" } } ################################################################################ exit 0; # Useless, but Perl doesn't see that this filehandle is used more than once close NULL; # Automatically closed by `open3' ################################################################################ # Transform codec codes into proper names # TODO: Find more codes sub video_codec { given ($_[0]) { when /^0x10000001$/i { return 'MPEG-1'; } when /^0x10000002$/i { return 'MPEG-2'; } when /^(MPG4|MP4V)$/i { return 'MPEG-4'; } when /^MP42$/i { return 'MS MPEG-4 v2'; } when /^MP43$/i { return 'MS MPEG-4 v3'; } when /^div3$/i { return 'DivX3'; } when /^DIV(4|X)$/i { return 'DivX4'; } when /^DX50$/i { return 'DivX5'; } when /^XVID$/i { return 'XviD'; } when /^(avc1|H264)$/i { return 'H.264'; } when /^VP62$/i { return 'VP6'; } when /^RV40$/i { return 'rv40'; } default { return $_[0]; } } } sub audio_codec { given ($_[0]) { when /^1$/i { return 'PCM'; } when /^2$/i { return 'MS ADPCM'; } when /^85$/i { return 'MP3'; } when /^8192$/i { return 'AC-3'; } when /^353$/i { return 'WMA v2'; } when /^MP4A$/i { return 'AAC'; } when /^22127$/i { return 'Vorbis'; } default { return $_[0]; } } } # Convert ISO 639 language codes into full names sub code2lang { my $code = $_[0]; my $lang; $lang = code2language ($code, LOCALE_LANG_ALPHA_2); return $lang if defined $lang; $lang = code2language ($code, LOCALE_LANG_ALPHA_3); return $lang if defined $lang; return $code; } sub mkcast { no warnings 'uninitialized'; return $_->{name}. '::' .$_->{role}. '::imdb:' .$_->{id}; }