summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xvideoadd.pl311
-rwxr-xr-xvideodb-check.pl257
-rw-r--r--videodb.rc2
-rwxr-xr-xvideomv.pl686
4 files changed, 1256 insertions, 0 deletions
diff --git a/videoadd.pl b/videoadd.pl
new file mode 100755
index 0000000..add8f2a
--- /dev/null
+++ b/videoadd.pl
@@ -0,0 +1,311 @@
+#!/usr/bin/perl -w
+
+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 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 videodata
+ imdb url/;
+
+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/\/*$//;
+
+################################################################################
+
+# videoadd [--seen] [--dont-sort] [--search=(title|imdbid)]
+# [-o title=...] [ -o year=...]
+# file
+
+################################################################################
+
+my $ignoredb_flag;
+my $sort_flag = 1;
+my %options;
+GetOptions( "seen" => sub { $options{seen} = 1 }
+ , "s|search=s"=> sub { $imdb{crit} = $_[1] }
+# , "u|update=s"=> update id/filename
+ , "o=s" => sub { my ($k,$v) = split /=/, $_[1], 2;
+ $options{lc $k} = $v; }
+ , "dont-sort" => sub { undef $sort_flag }
+ , "ignore-db" => \$ignoredb_flag
+ , "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;
+
+
+################################################################################
+# 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}";
+ $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 {
+ # Bug, see https://rt.cpan.org/Public/Bug/Display.html?id=71429
+ open my $STDOUT, '>&', \*STDOUT or die "Can't dup: $!";
+ open STDOUT, '>', '/dev/null' or die "Cannot open `/dev/null': $!";
+ $movie = new IMDB::Film(%imdb);
+ close STDOUT or die "Can't close";
+ open STDOUT, '>>&', $STDOUT or die "Cannot open: $!";
+ die "Something wrong happened: " .$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 {$_->{name}. '::' .$_->{role}. '::imdb:' .$_->{id}}
+ @{$movie->cast()};
+ $new{country} = join ', ', @{$movie->country()};
+ $new{plot} = $movie->storyline();
+ $new{rating} = $movie->rating(); # Ignoring #votes and awards
+
+ $new{istv} = 1 if $movie->kind() =~ /tv/;
+}
+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};
+ $new{custom1} = lc join (', ', @slang);
+}
+
+
+################################################################################
+# Insertion into the database
+
+unless ( defined $ignoredb_flag ) {
+ my $INSERT = "INSERT INTO $config{videodata}
+ SET mediatype = 14";
+ if (defined $new{filedate}) {
+ $INSERT .= ", filedate = FROM_UNIXTIME($new{filedate})";
+ delete $new{filedate};
+ }
+
+ while (my ($k,$v) = each %new) {
+ $INSERT .= ", " .$k. " = " .$dbh->quote ($v);
+ }
+ $dbh->do($INSERT) or die "Can't insert: $!\n";
+# print $INSERT, "\n";
+
+ my $ids = $dbh->selectall_arrayref ( "SELECT id FROM $config{videodata}
+ WHERE filename = ? ",
+ undef, $new{filename} );
+ if ($#$ids == 0) {
+ print LOG "Check it out! ",
+ $config{url}, "/show.php?id=", $ids->[0]->[0], "\n"
+ or die "Can't print: $!";
+ }
+ else {
+ warn "Warning: Something weird happened during the INSERT.\n";
+ }
+
+ $dbh->disconnect;
+}
+
+
+
+################################################################################
+# Sort the file
+
+if ( defined ($file) and defined ($sort_flag) ) {
+ if (defined $new{director}) {
+ move ( $file, catfile ( $directors, $new{director}, $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;
+}
diff --git a/videodb-check.pl b/videodb-check.pl
new file mode 100755
index 0000000..8acff46
--- /dev/null
+++ b/videodb-check.pl
@@ -0,0 +1,257 @@
+#!/usr/bin/perl -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, 27 November 2011";
+
+use DBI;
+use Pod::Usage;
+use File::Spec::Functions;
+use Cwd;
+use Env qw /HOME/;
+use strict;
+
+################################################################################
+
+# Configuration
+my $confile = catfile ($HOME, '.videodb.rc');
+die "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 videodata/;
+
+my $symlinks = catdir($config{videodir},'MOVIES'); # Symlinks folder
+die "Error: No such directory: `" .$symlinks. "'.\n" unless -d $symlinks;
+
+################################################################################
+
+=head1 NAME
+
+videodb-check.pl - a sanity check for your video collection
+
+=head1 SYNOPSIS
+
+B<videodb-check.pl>
+
+=head1 DISCLAIMER
+
+Your collection is assumed to have the following structure: two
+folders, I<DIRECTORS> and I<MOVIES>, that have the same parent.
+I<DIRECTORS> contains one subdirectory for each director, and each movie
+lies (B<as a regular file>) in the subdirectory of its director.
+The folder I<MOVIES> contains symlinks - one for each movie - that target
+to I<../DIRECTORS/director_of_the_movie/movie>.
+The behavior of B<videodb-check.pl> 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<videodb-check> performs the following sanity checks on your database
+and collection:
+
+=over 4
+
+=item *
+
+Each entry in the database has a corresponding symlink under the
+directory I<MOVIES> of your collection, that in turn has a valid target
+in the directory I<DIRECTORS>.
+
+=item *
+
+The directory in I<DIRECTORS> coincides with the director(s) found in
+the database.
+
+=item *
+
+Each symlink in the directory I<MOVIES> of your collection has exactly
+one corresponding entry in the database.
+
+=item *
+
+No movie in the database has invalid release date, empty title or empty
+language.
+
+=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 => '******',
+ videodata => "videodb_videodata",
+
+=head1 EXIT STATUS
+
+The exit status is 0 if your collection and database are sane, and 1
+otherwise.
+
+=head1 REQUIREMENTS
+
+The imported modules are available on CPAN. See the source for details.
+
+=head1 AUTHOR
+
+Copyright 2011-2012 Guilhem Moulin. See the source for copying conditions.
+
+=cut
+
+################################################################################
+
+if (@ARGV) {
+ pod2usage(-exitstatus => 0, -verbose => 2) if $ARGV[0] eq '--man';
+ pod2usage(2);
+}
+*LOG = *STDOUT;
+
+################################################################################
+
+# Connect to database
+my $dsn = "DBI:$config{driver}:database=$config{database};host=$config{hostname};port=$config{port}";
+my $dbh = DBI->connect($dsn, $config{user}, $config{password})
+ or die "Can't connect do database\n";
+$dbh->do( "set names utf8" ) or die;
+
+################################################################################
+
+# Check that all entries in the DB have a symlink, that in turn have a
+# valid target
+
+my $r = 0; # Exit status
+my $res = $dbh->selectall_arrayref ( "SELECT filename,director FROM $config{videodata}" )
+ or die "Can't select: " .$dbh->errstr. "\n";
+
+
+my @links; # List of missing symlinks
+my @files; # List of symlinks that have a non existing/valid target
+my @dirs; # List of symlinks that target to a wrong director
+foreach (@$res) {
+ my @ls = File::Spec->splitdir($_->[0]);
+ my $l = File::Spec->catfile ($symlinks, $ls[0]);
+
+ unless (-l $l) {
+ push @links, $_->[0];
+ next;
+ }
+
+ my @target = File::Spec->splitdir (readlink $l);
+ push @files, $_->[0]
+ unless ( $target[0] eq File::Spec->updir()
+ and $target[1] eq "DIRECTORS"
+ and -f File::Spec->catfile ($symlinks, $_->[0]) );
+
+ if ( not (defined $_->[1]) or $_->[1] eq '' ) {
+ push @dirs, $_->[0]. " (no director)";
+ }
+ elsif ( $target[2] ne $_->[1] ) {
+ push @dirs, $_->[0]. " (" .$target[2]. " <> " .$_->[1]. ")";
+ }
+}
+
+
+&report ("The following entries are in the DB, but I can't find the files", \@links);
+&report ("The following entries don't have a valid target", \@files);
+&report ("The following entries target to a wrong director", \@dirs);
+
+
+################################################################################
+
+# Check that each symlink has exactly one entry in the DB
+
+my @filelist; # List of filenames that have <> 1 entry in the DB
+opendir (DIR, $symlinks) or die "Can't open dir `" .$symlinks. ".:" .$!. "\n";
+while (my $l = readdir(DIR)) {
+ next if $l eq File::Spec->curdir();
+ next if $l eq File::Spec->updir();
+
+ my $f = File::Spec->catfile($symlinks, $l);
+ if ( -d Cwd::realpath($f) ) {
+ opendir (SUBDIR, Cwd::realpath($f))
+ or die "Can't open dir `" .Cwd::realpath($f). ".:" .$!. "\n";
+ while (my $d = readdir(SUBDIR)) {
+ next if $d eq File::Spec->curdir();
+ next if $d eq File::Spec->updir();
+ push @filelist, File::Spec->catfile($l,$d);
+ }
+ closedir(SUBDIR) or die "Can't close: $!\n";
+ }
+ elsif ( -l $f or -f $f ) {
+ push @filelist, $l;
+ }
+}
+closedir(DIR) or die "Can't close: $!\n";
+
+
+undef @files;
+my $sth = $dbh->prepare ( "SELECT id FROM $config{videodata} WHERE filename = ?" )
+ or die "Error: " .$dbh->errstr;
+foreach (@filelist) {
+ $sth->execute ($_) or die "Can't select: " .$dbh->errstr. "\n";
+ my @res = $sth->fetchrow_array;
+ die $sth->errstr if $sth->err;
+
+ push @files, $_ unless $#res == 0;
+}
+$sth->finish;
+&report ("The following files have <> 1 corresponding entries in the DB", \@files);
+
+
+&check ( "no release date", "year = 0" );
+&check ( "no language set", "language IS NULL OR language =''" );
+&check ( "no title set", "title IS NULL OR title =''" );
+
+
+################################################################################
+
+
+$dbh->disconnect;
+exit $r;
+
+
+################################################################################
+
+
+sub report {
+ my ($str, $bad) = @_;
+ if (@$bad) {
+ print LOG "* " .$str. ":\n";
+ foreach (@$bad) {
+ print LOG " ", $_, "\n";
+ }
+ $r = 1;
+ }
+}
+
+sub check {
+ my $bad = $dbh->selectall_arrayref( "SELECT filename
+ FROM $config{videodata}
+ WHERE " .$_[1] )
+ or die "Can't select: " .$dbh->errstr. "\n";
+ &report ("The following files have " .$_[0], [map {$_->[0]} @$bad]);
+}
diff --git a/videodb.rc b/videodb.rc
index 198b60a..8a3ce70 100644
--- a/videodb.rc
+++ b/videodb.rc
@@ -11,3 +11,5 @@ user => 'username',
port => 3306,
password => '******',
videodata => "videodb_videodata",
+imdb => 'akas.imdb.com',
+url => 'https://videodb.example.org'
diff --git a/videomv.pl b/videomv.pl
new file mode 100755
index 0000000..a21bc33
--- /dev/null
+++ b/videomv.pl
@@ -0,0 +1,686 @@
+#! /usr/bin/perl -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.2, 10 August 2011";
+
+use Getopt::Long qw/:config posix_default no_ignore_case gnu_compat
+ bundling auto_version auto_help/;
+use Pod::Usage;
+use DBI;
+use File::Basename;
+use File::Spec::Functions qw /catfile catdir splitdir updir/;
+use File::Copy;
+use Cwd qw /realpath/;
+use Env qw /HOME/;
+use strict;
+
+################################################################################
+
+# Configuration
+my $confile = catfile ($HOME, '.videodb.rc');
+die "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 videodata/;
+
+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;
+
+
+################################################################################
+
+=head1 NAME
+
+videomv.pl - move your videos to or within your collection
+
+=head1 SYNOPSIS
+
+B<videomv.pl> [B<--lookup-db>] [B<--ignore-db>] [B<-q>]
+I<path/to/oldfile> [{I<MOVIES>,I<DIRECTORS/director/>}I</>[I<newfile>]]
+
+=head1 DISCLAIMER
+
+Your collection is assumed to have the following structure: two
+folders, I<DIRECTORS> and I<MOVIES>, that have the same parent.
+I<DIRECTORS> contains one subdirectory for each director, and each movie
+lies (B<as a regular file>) in the subdirectory of its director.
+The folder I<MOVIES> contains symlinks - one for each movie - that target
+to I<../DIRECTORS/director_of_the_movie/movie>.
+The behavior of B<videomv.pl> 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
+
+Depending on whether I<path/to/> is I<MOVIES/>,
+I<DIRECTORS/*/>, or something
+else, you will get one of the six behaviors below. For the sake of
+readability the paths are relative to the root of your video collection
+here, but hopefully other origins or
+paths that contain symlinks are fine as well.
+
+=over 4
+
+=item B<videomv.pl> [B<--lookup-db>] I<MOVIES/oldfile> I<MOVIES/newfile>
+
+I<oldfile> is expected to be an existing symlink in I<MOVIES/>,
+which targets to I<../DIRECTORS/director/oldfile>. If
+I<oldfile>=I<newfile>, an error will be raised. Otherwise, the following
+actions will be performed:
+
+=over 4
+
+=item *
+
+The old target will be renamed to I<DIRECTORS/director/newfile>,
+
+=item *
+
+The old symlink will be deleted,
+
+=item *
+
+A new symlink I<MOVIES/newfile> -> I<../DIRECTORS/director/newfile>
+will be created, and
+
+=item *
+
+The filename in the database will be updated.
+
+=back
+
+If the B<--lookup-db> flag is set, the new I<director> will be found on the
+database instead of by parsing the path of the old target.
+
+
+=item B<videomv.pl> I<MOVIES/oldfile> I<DIRECTORS/newdirector/>[I<newfile>]
+
+I<oldfile> is expected to be an existing symlink in I<MOVIES/>,
+which targets to I<../DIRECTORS/olddirector/oldfile>. If
+I<olddirector/oldfile>=I<newdirector/newfile>
+(or if I<olddirector>=I<newdirector> and I<newfile> was not given),
+an error will be raised.
+Otherwise, the following actions will be performed:
+
+=over 4
+
+=item *
+
+The old target will be moved to I<DIRECTORS/newdirector/newfile>,
+(or to I<DIRECTORS/newdirector/oldfile> if I<newfile> was not given),
+
+=item *
+
+The old symlink will be deleted,
+
+=item *
+
+A new symlink I<MOVIES/newfile> -> I<../DIRECTORS/newdirector/newfile>
+(or -> I<../DIRECTORS/newdirector/oldfile> if I<newfile> was not given)
+will be created, and
+
+=item *
+
+The filename in the database will be updated unless I<oldfile>=I<newfile>.
+
+=back
+
+
+=item B<videomv.pl> [B<--lookup-db>] I<DIRECTORS/director/oldfile> I<MOVIES/newfile>
+
+A symlink I<MOVIES/oldfile> -> I<../DIRECTORS/director/oldfile> is
+expected to exist.
+If I<oldfile>=I<newfile>, an error will be raised. Otherwise, the following
+actions will be performed:
+
+=over 4
+
+=item *
+
+The old target I<DIRECTORS/director/oldfile> will be renamed to
+I<DIRECTORS/director/newfile>,
+
+=item *
+
+The old symlink will be deleted,
+
+=item *
+
+A new symlink I<MOVIES/newfile> -> I<../DIRECTORS/director/newfile>
+will be created, and
+
+=item *
+
+The filename in the database will be updated.
+
+=back
+
+If the B<--lookup-db> flag is set, the new I<director> will be found on the
+database instead of by parsing the path of the old target.
+
+
+=item B<videomv.pl> I<DIRECTORS/olddirector/oldfile> I<DIRECTOR/newdirector/>[I<newfile>]
+
+A symlink I<MOVIES/oldfile> -> I<../DIRECTORS/olddirector/oldfile> is
+expected to exist.
+If I<olddirector/oldfile>=I<newdirector/newfile>
+(or if I<olddirector>=I<newdirector> and I<newfile> was not given),
+an error will be raised.
+Otherwise, the following actions will be performed:
+
+=over 4
+
+=item *
+
+The old target I<../DIRECTORS/olddirector/oldfile> will be moved to
+I<../DIRECTORS/newdirector/newfile>,
+
+=item *
+
+The old symlink will be deleted,
+
+=item *
+
+A new symlink I<MOVIES/newfile> -> I<../DIRECTORS/newdirector/newfile>
+will be created, and
+
+=item *
+
+The filename in the database will be updated unless I<oldfile>=I<newfile>.
+
+=back
+
+
+=item B<videomv.pl> B<--lookup-db> I<path/to/oldfile> [I<MOVIES>[I</newfile>]]
+
+Where I<path/to/> is neither of I<MOVIES/> nor I<DIRECTORS/*/>,
+and I<oldfile> is a regular file.
+The I<director> will found in the database, and and error will be
+raised if no entry is (or multiple entries are) found.
+In case of success, the following actions will be performed:
+
+=over 4
+
+=item *
+
+I<oldfile> will be moved to I<DIRECTORS/director/newfile>
+(or to I<DIRECTORS/director/oldfile> if I<newfile> was not given),
+
+=item *
+
+A symlink I<MOVIES/newfile> -> I<../DIRECTORS/director/newfile>
+(or I<MOVIES/oldfile> -> I<../DIRECTORS/director/oldfile> if I<newfile>
+was not given) will be created, and
+
+=item *
+
+The filename in the database will be updated unless I<oldfile>=I<newfile>.
+
+=back
+
+Note that the B<--lookup-db> flag has to be set for this to work; if not, an error
+will be raised.
+
+
+=item B<videomv.pl> I<path/to/oldfile> I<DIRECTOR/director/>[I<newfile>]
+
+Where I<path/to/> is neither of I<MOVIES/> nor I<DIRECTORS/*/>,
+and I<oldfile> is a regular file.
+The following actions will be performed:
+
+=over 4
+
+=item *
+
+I<oldfile> will be moved to I<DIRECTORS/director/newfile>
+(or to I<DIRECTORS/director/oldfile> if I<newfile> was not given),
+
+=item *
+
+A symlink I<MOVIES/newfile> -> I<../DIRECTORS/director/newfile>
+(or I<MOVIES/oldfile> -> I<../DIRECTORS/director/oldfile> if the second
+argument was not given) will be created,
+
+=item *
+
+The filename in the database will be updated unless I<oldfile>=I<newfile>.
+
+=back
+
+=back
+
+The actions above will be performed in the given order. If some action
+fails, B<videomv.pl> will try to revert the successful ones (in reverse
+order).
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--lookup-db>
+
+If the second argument is in I<MOVIES/*/> and this flag is set, B<videomv.pl>
+will search the I<director> in the data base. An error will be raied if
+no entry is (or multiple entries are) found. Look above for details.
+
+=item B<--ignore-db>
+
+Do not connect to the database, (thus do not update it with the new
+filename). This flag is incompatible with B<--lookup-db>.
+
+=item B<-q>, B<--quiet>
+
+By default, B<videomv.pl> prints each action it performs, with their
+return status. This flag supresses this behavior. Use at your own risk.
+
+=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 => '******',
+ videodata => "videodb_videodata",
+
+=head1 EXIT STATUS
+
+The exit status is 0 if all the actions went through, 1 if some error
+happened, and 2 if some action fails and the successful ones have been
+smoothly reverted.
+
+=head1 REQUIREMENTS
+
+The imported modules are available on CPAN. See the source for details.
+
+=head1 AUTHOR
+
+Copyright 2011 Guilhem Moulin. See the source for copying conditions.
+
+=cut
+
+################################################################################
+
+
+#TODO: overwrite existing files?
+#TODO: explore depth>1 in DIRECTORS/
+
+my $lookupdb_flag;
+my $ignoredb_flag;
+
+# Get options
+GetOptions( "lookup-db"=> \$lookupdb_flag,
+ "ignore-db"=> \$ignoredb_flag,
+ "q|quiet" => 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 or $#ARGV > 1);
+*LOG = *STDERR unless defined (fileno LOG);
+
+my ($old_path,$new_path) = @ARGV;
+$new_path = $symlinks unless defined $new_path;
+
+die "Error: incompatible options.\n" if
+ (defined $lookupdb_flag) and (defined $ignoredb_flag);
+
+die "Error: `" .$old_path. "' is neither a symlink nor a plain file.\n"
+ unless (-l $old_path or -f $old_path);
+
+$new_path = catfile ($new_path, basename($old_path))
+ if -d realpath($new_path);
+die "Error: `" .$new_path. "' exists.\n"
+ if (-l $new_path or -f $new_path);
+
+my $old_filename = basename($old_path);
+my $new_filename = basename($new_path);
+
+# Get the real paths, {..,symlink}-collapse and so on
+my $real_old_path = catfile (realpath (dirname($old_path)), $old_filename);
+my $real_new_path = catfile (realpath (dirname($new_path)), $new_filename);
+
+my $real_symlinks = realpath($symlinks);
+my @real_symlinks = splitdir($real_symlinks);
+my $real_directors = realpath($directors);
+my @real_directors = splitdir($real_directors);
+
+
+# Connect to database
+my ($dsn, $dbh, $RES, $nRES, $id);
+unless (defined $ignoredb_flag) {
+ $dsn = "DBI:$config{driver}:database=$config{database};host=$config{hostname};port=$config{port}";
+ $dbh = DBI->connect($dsn, $config{user}, $config{password})
+ or die "Can't connect do database";
+ $dbh->do( "set names utf8" ) or die;
+
+
+ # Lookup for the file in the videodb database
+ $RES =
+ $dbh->selectall_hashref ( "SELECT id,director,title FROM $config{videodata}
+ WHERE filename = ?",
+ 'id', undef, $old_filename
+ )
+ or die "Can't select: " .$dbh->errstr. "\n";
+ $nRES = scalar (keys %$RES);
+
+ # The ID of the first movie found in the database, if any
+ $id = each %$RES;
+}
+
+my $r = 0;
+my ($old_director, $new_director);
+my ($old_symlink, $new_symlink, $new_target, $old_target);
+
+if (&is_symlink($real_new_path)) {
+
+ # The destination is under $symlinks
+
+ if (defined $lookupdb_flag) {
+ # Find the new director on the database
+
+ # Ensure there is exactly one entry in the db
+ die "Error: No entry found in the database while the flag `--lookup-db' was set.\n"
+ if $nRES == 0;
+ die "Error: Multiples entries found in the database while the flag `--lookup-db' was set.\n"
+ if $nRES > 1;
+
+ $new_director = $RES->{$id}->{director};
+ $new_director =~ s/ ?: ?/ -/;
+ $new_director =~ tr@/@_@;
+ $new_director =~ s/[[:cntrl:]]//;
+
+ # Create directory if it doesn't exist
+ my $new_dirname = catfile ($directors, $new_director);
+ unless (-d $new_dirname) {
+ print STDERR "Directory `$new_dirname' does not exist. ";
+ until (-d $new_dirname) {
+ print STDERR "Should I create it? (Y/n) ";
+ my $a = lc <STDIN>;
+ chomp $a;
+ if ($a eq 'y' or $a eq '') {
+ mkdir $new_dirname
+ or die "Error: Cannot mkdir `$new_dirname': $!\n";
+ }
+ elsif ($a eq 'n') {
+ exit 0;
+ }
+ }
+ }
+ }
+
+ $new_symlink = $new_path;
+
+ if (&is_symlink($real_old_path)) {
+
+ $old_symlink = $old_path;
+
+ # Source is presumably a symlink: ensure it is
+ die "Error: `" .$old_path. "' is expected to be a symlink.\n"
+ unless -l $real_old_path;
+
+ $old_target = realpath ($old_path);
+ die "Error: `" .$old_path. "' is expected to target to `"
+ .catfile(updir(),'DIRECTORS','*',$old_filename). "'.\n"
+ unless (defined $new_director
+ || &is_director(\$new_director, $old_target));
+ $new_target = catfile ($directors, $new_director, $new_filename);
+ }
+
+ elsif (&is_director(\$old_director, $real_old_path)) {
+
+ $old_symlink = catfile ($symlinks, $old_filename);
+ die "Error: `" .$old_symlink. "' is expected to be a symlink.\n"
+ unless -l $old_symlink;
+ die "Error: `" .$old_symlink. "' is expected to target to `"
+ .catfile(updir(),'DIRECTORS',$old_director,$old_filename)
+ ."'.\n"
+ unless realpath($old_symlink) eq realpath($old_path);
+
+ $old_target = $old_path;
+
+ $new_director = $old_director unless defined $new_director;
+ $new_target = catfile ($directors, $new_director, $new_filename);
+ }
+
+ else {
+ $old_target = $old_path;
+ die "Error: `" .$old_path. "' is expected to be a regular file.\n"
+ unless -f $old_path;
+ die "Error: Dunno where to put this file. Try the `--lookup-db' flag.\n"
+ unless defined $new_director;
+
+ $new_target = catfile ($directors, $new_director, $new_filename);
+ }
+}
+
+elsif (&is_director(\$new_director, $real_new_path)) {
+
+ # The destination is under $directors/*
+
+ $new_target = $new_path;
+ $new_symlink = catfile ($symlinks, $new_filename);
+
+ if (&is_symlink($real_old_path)) {
+
+ # Source is presumably a symlink: ensure it is
+ die "Error: `" .$old_path. "' is expected to be a symlink.\n"
+ unless -l $real_old_path;
+
+ $old_symlink = $old_path;
+ $old_target = realpath ($old_path);
+ }
+
+ elsif (&is_director(\$old_director, $real_old_path)) {
+
+ $old_symlink = catfile ($symlinks, $old_filename);
+ die "Error: `" .$old_symlink. "' is expected to be a symlink.\n"
+ unless -l $old_symlink;
+ die "Error: `" .$old_symlink. "' is expected to target to `"
+ .catfile(updir(),'DIRECTORS',$old_director,$old_filename)
+ ."'.\n"
+ unless realpath($old_symlink) eq realpath($old_path);
+
+ $old_target = $old_path;
+ }
+
+ else {
+ $old_target = $old_path;
+ die "Error: `" .$old_path. "' is expected to be a regular file.\n"
+ unless -f $old_path;
+ }
+}
+else {
+ die "Error: destination has to be in `"
+ .$symlinks. "' or in `" .catfile($directors,'*'). "'.\n"
+ ."See `" .$0. " --man' for details\n";
+}
+
+
+my @actions; # Successful actions
+
+if (dirname (realpath $old_target) eq dirname (realpath $new_target)) {
+ &perform ("Renaming target... ", rename $old_target, $new_target)
+ unless ($old_filename eq $new_filename);
+}
+else {
+ &perform ("Moving target... ", move $old_target, $new_target);
+}
+
+my $old_tar;
+if (defined $old_symlink) {
+ $old_tar = readlink $old_symlink;
+ &perform ("Deleting old symlink... ", unlink $old_symlink);
+}
+
+
+&perform ("Creating new symlink... ",
+ symlink catfile(updir(),'DIRECTORS',$new_director,$new_filename),
+ $new_symlink);
+
+
+unless (defined $ignoredb_flag || $old_filename eq $new_filename) {
+ print LOG "Updating database... ";
+ if ($nRES == 0) {
+ $r = 2;
+ print LOG "failed!: no entry found.\n";
+ &revert(@actions);
+ }
+ elsif ($nRES > 1) {
+ $r = 2;
+ print LOG "failed!: multiple entries found.\n";
+ &revert(@actions);
+ }
+ else {
+ my $rv = $dbh->do ( "UPDATE $config{videodata} SET filename = ? WHERE id = ?",
+ undef, $new_filename, $id
+ );
+ &ack (\$r, $rv);
+ }
+}
+
+
+# Disconnect
+$dbh->disconnect() unless defined $ignoredb_flag;
+
+exit $r;
+
+
+
+################################################################################
+
+
+
+# Acknowledge the result of a test
+sub ack {
+ my ($r, $test) = @_;
+ if ($test) {
+ print LOG "OK\n";
+ return 0;
+ } else {
+ print LOG "failed!: $!\n";
+ $$r = 2 if defined $r;
+ return 1;
+ }
+}
+
+# Perform the given action; revert successful changes in case of failing
+sub perform {
+ return if $r;
+
+ my ($action,$test) = @_;
+
+ print LOG $action;
+ if (&ack (\$r, $test)) {
+ &revert(@actions);
+ }
+ else {
+ push @actions, $action;
+ }
+}
+
+# Revert successful changes
+sub revert {
+ my @actions = reverse @_;
+
+ return if $#actions < 0;
+
+ print LOG "\n";
+ print STDERR "Failed!: reverting successful changes...\n";
+
+ while ($#actions >= 0) {
+ my $rev;
+ if ($actions[0] =~ /^(Renaming target)(\.{3}) {5}( *)/) {
+ $actions[0] = $1 .' back'. $2.$3;
+ $rev = rename $new_target, $old_target;
+ }
+ elsif ($actions[0] =~ /^(Moving target)(\.{3}) {5}( *)/) {
+ $actions[0] = $1 .' back'. $2.$3;
+ $rev = move $new_target, $old_target;
+ }
+ elsif ($actions[0] =~ /^Deleting( old symlink.*)/) {
+ $actions[0] = 'Creating'. $1;
+ $rev = symlink $old_tar, $old_symlink;
+ }
+ elsif ($actions[0] =~ /^Creating( new symlink.*)/) {
+ $actions[0] = 'Deleting'. $1;
+ $rev = unlink $new_symlink;
+ }
+ else {
+ print STDERR "Error: unknown action `" .$actions[0]. "'!\n";
+ $actions[0] = undef;
+ $r = 1;
+ }
+
+ if (defined $actions[0]) {
+ print LOG $actions[0];
+ &ack (\$r, $rev);
+ }
+ shift @actions;
+ }
+}
+
+# Check wether a path starts with $movies
+sub is_symlink {
+ my @path = splitdir ($_[0]);
+ my @real_symlinks = @real_symlinks; #local copy
+
+ while ($#real_symlinks>=0 && $#path>=0 && $real_symlinks[0] eq $path[0]) {
+ shift @real_symlinks;
+ shift @path;
+ };
+
+ if ($#real_symlinks<0 && $#path==0) {
+ return 1; # That's a "symlink"
+ } else {
+ return 0; # That's not a "symlink"
+ }
+}
+
+# Check wether a path starts with $director/dir, and put `dir' in the
+# first argument if that's the case
+sub is_director {
+ my @path = splitdir ($_[1]);
+ my @real_directors = @real_directors; #local copy
+
+ while ($#real_directors>=0 && $#path>=0 && $real_directors[0] eq $path[0]) {
+ shift @real_directors;
+ shift @path;
+ };
+
+ if ($#real_directors<0 && $#path>=0) {
+ ${$_[0]} = $path[0] if defined $_[0];
+ return 1; # That's a "director"
+ } else {
+ return 0; # That's not a "director"
+ }
+}