#!/usr/bin/perl -I/usr/depot/lib/perl5

my $TITLE = "PSYC media console 4.1";
my $AUTHOR = "by the symbolic lynX\@psycamp.pages.de";

# see end of file for end-user-oriented documentation
#
# uses 'mplayer' - the most popular media player on linux with the
# worst scripting API ever seen in a lifetime. it is documented
# in mplayer's source distribution in DOC/tech/slave.txt.
#
# can alternatively be configured back to use the 'rxaudio' server
# engine from http://www.mpeg.org/xaudio/ which has a lot more
# reasonable API, but it only plays mp3 - the copy that I have 
# been using since 1997 or so has a sha256sum of
# ddb096ad42d9b6b543db8a3a6d9b4a9d52943e75e96697dbbadbc779140c498e.
# although the general public never saw any source codes to it, it is
# viable to assume that it's not a trojan. grab a copy from 
# http://mp3.pages.de/files/rxaudio
#
# psycamp requires the Net/PSYC.pm module since it uses its event 
# multiplexing abilities, not just to receive PSYC messages, but
# also to handle stdin and engine input in parallel.
#
# things still in "makenoise":	marks, volume levels.

# since perl has no native preprocessor, this code is
# managed by the 'jaggler' preprocessor.
#
# list of available jaggler flags:
#  'T' := "time" - activates output of access and modification time
#  'O' := "org" - enable shift-key functions to reorganize collection
#  'X' := "rxaudio" - use the old mp3 engine instead of mplayer
#
# psycamp in 'distribution' default mode
#	jaggler -x -c# -j% -DO psycamp
# psycamp supporting 'T' and 'O' extras:
#	jaggler -x -c# -j% -DOT psycamp

# HINTS & HACKS:
# in order to play only high quality files from a folder, you can use
#	"lm -Lb 193 >/tmp/playlist-$USER.m3u; psycamp"
# lm is available from http://perl.pages.de. such functionality could
# obviously be integrated into here, allowing us also to remove file
# type guessing - but that would slow down the playlist creation
# process, if every file were thrown at ffmpeg to discover its media
# properties.

# default PSYC address for this service, UDP port 1144 on this host.
# can be overridden with -b
#
$UNI = 'psyc://127.0.0.1:1144d/';
#
# be mindful that if you use outgoing -M the receiving side can send
# remote control commands back to you, even if you bind localhost here.
# you may like to have such chat-based remote control, or you may not..

# volume values, since volume doesn't seem to be linear as it should be.
# at least not my soundcard, check for yourself..
#
@VV = ( 0,2,5,8,11,14,17,20,24,28,33,38,43,49,56,64,72,81,90,100 );

# would prefer not to enumerate media formats understood by mplayer,
# but it is tricky to sort out non-media files when recursively
# spidering input directories.
#
# when running rxaudio, psycamp can only handle .mp3 and .sdj
#   "part" and "dl" are the temporary filenames of some download tools
$FILETYPES =
#%	"(mp3|sdj|part|dl)";					    #? X
	"(mp\\d|sdj|part|dl|flac|wav|aif|aiff|ogg|m4a|aac|opus|au"  #? !X
# whereas mplayer can also handle video
	. "|webm|mkv|mov|wmv|avi|asf|flv|vob|ogv|qt|yuv|rm|m\dv|mp\w|mpeg|3gp|3g2|mxf|f4\w)";  #? !X
#
# 'makenoise' had support for these formats:
# "(sea|mod|gz|lha|Z|lzh|zip|s3m)"; # |med|mmd0)";
# should also be able to handle .pls..?

# starting pcm volume. system volume is kept at maximum anyway.
#%$VOL = 0;	#? X
$VOL = 100;	#? !X

# allow 'D' key to delete the file currently being listened to (!!)
# ignored if -DO was not provided in jaggler
$ALLOW_DELETE_KEY = 1;

# string contained in path for files that are deleted in -d mode
$VOLATILE = 'VOLATILE';

# string contained in path for files that are kept even when in -D mode
$KEEP = 'LOCAL';

# debugging (inline macro, if undefined all debugging code is removed)
#	1: misc debug
#	2: debug file recursion
#	4: debug randomizer
#	8: show playlist order
#	16: debug PSYC transactions
#	32: weird things that shouldn't happen
#	64: show server pid
#	128: show tracks of same size when using -s S
#	256: show all output from media engine
#	512: watch only engine parse
#	1024: watch timer events (timeout simulation)
#	2048: show engine debug messages
#	4096: examine progress
sub DEBUG () { 32 + 128 }

# also activate PSYC debugging
#Net::PSYC::setDEBUG(3);

# used by randomize algorithm - how much of the path is compared?
# would be smart to choose this value dynamically..
sub PATHMATCH () { 12 }

# file to put media information into about files that got trashed
sub HATEINDEX () { "$ENV{HOME}/.media/TRASH-$ENV{HOST}.ix" }

# default output for video is the main screen, so if you have
# a second monitor, it makes sense to run psycamp over there
my $screen = 0;


### BEGINNING OF REGULAR CODE

	require 5.000;
	use POSIX ":sys_wait_h";
	use Pod::Usage qw( pod2usage );
	use Getopt::Std;
	use Cwd qw(chdir);  # maintains PWD in ENV
	use File::Path qw(mkpath);
	use File::Find ();
	use FileHandle;
	use Carp;
	#use MPEG::MP3Info;	# no longer necessary
	use Date::Format qw( time2str );    ## T
	use IPC::Open3 qw( open3 );
	use Net::PSYC qw( :event );
	# would need to extend this to actually use it:
	#use Audio::Play::MPlayer;
	use Term::ANSIColor qw( :constants );
	use Term::ReadKey qw(GetTerminalSize);
	($TWIDTH, $THEIGHT) = GetTerminalSize();

	*name   = *File::Find::name;	# ugly style works
	my %I;
	my $CDUR = 0;

	$tmpdir='/temp';
	$tmpdir='/tmp' unless -d $tmpdir and -w _;
	$tmpdir='.' unless -d $tmpdir and -w _;
	# $tmplock="$tmpdir/.psycamp-copylock";
	$playlist="$tmpdir/playlist-$ENV{USER}.m3u";

MAIN: {
	if ($#ARGV >= 0) {
		getopt('bMnsS');
	}
	$nick = $opt_n
	     || $ENV{'PSYCNICK'}
	     || $ENV{'NICK'}    # this one should work with any chat system
	     || $ENV{'IRCNICK'}
	     || $ENV{'USER'}
	     || $ENV{'HOST'}
	     || 'unixer';

	print "Using playlist: $playlist\n" if $opt_v;
	if ($opt_h) {
		print BOLD, BLACK, &head, RESET, "\n";
# Old options removed from SYNOPSYS:
#	[-S]kip files if the path contains the word '$KEEP', dont play them.
#	[-l]ist filenames, sizes and bitrates (for archive documentation)
#	[-m]ono output
#	[-I]nitialize rxaudio anew for each song, special hack
		pod2usage;
		exit;
	}
	# initialize randomizer
#	my $a = time() ^ $$; $a = reverse $a; srand($a);
	# no longer necessary with newer perls,
	# even the following is optional:
	srand;

 #{ X
#%	print <<X unless $has_rxaudio = &which('rxaudio');
#%cannot find rxaudio. cannot play any mp3s without
#%rxaudio from http://www.mpeg.org/xaudio/
#%or, just for friends, from http://mp3.pages.de/files/	(old linux binary)
#%of course you are welcome to update psycamp to work with xmms or mplayer or..
#%
#%X
#%	# aoss: Wrapper to facilitate use of the ALSA OSS compatibility library.
#%	# in case you do not have it in form of kernel modules (snd-pcm-oss etc)
#%	# padsp: Wrapper to do the same with pulseaudio.
#%	$wrapper = &which('aoss') || &which('padsp') || "";
#%	print "Using wrapper: $wrapper\n" if $opt_v;
 #: X
#	$has_mpv = &which('mpv');
	print STDERR <<X unless $has_mplayer = &which('mplayer');
Cannot find mplayer. Cannot play media without it.

X
 #} X
	if ($opt_H ||
#%	    !$has_rxaudio	#? X
	    !$has_mplayer	#? !X
	) {
		# this block of text not moved into pod because of $UNI
		print BOLD, BLACK, &head, RESET, <<X;

This media player brings you a threefold functionality which you may combine
at will:

1. a command line media player which gives you possibilities to navigate
   media and similar functions by entering commands on the keyboard, so you
   don't need a GUI to achieve the same effects.

2. the player can be remote controlled with UDP messages according to the
   PSYC protocol for synchronous conferencing - an upcoming chat protocol
   which can be used for all sorts of messaging, so it's fine for this
   purpose too. This enables you to implement CGI-based remote controls
   or suchlike. the _request_execute method family is understood via PSYC.
   unless you specify the -b option, $UNI will be used
   as PSYC address for reception of commands. currently no authentication
   is requested, so it is generally good to bind to localhost. a message
   can contain several lines of instructions. no further input will be
   accepted while processing these instructions.

3. this player is scriptable by "scripting deejay" files (extension .sdj),
   they allow you to automate operations on media files, even simulate
   simple remixes without actually modifying the source material.

see '$0 -h' for usage instructions
X
		exit;
	}
	print STDERR <<X unless $has_ffmpeg = &which('ffmpeg');
Cannot find ffmpeg. Will not know duration and bitrates of some tracks.

X
	&enqueue(@ARGV);
	if ($NS) {		# global var for number of enqueued tracks
	    do {
		@order = $opt_r ? &randomize : &sorttracks($opt_s);
		print STDERR "\r[order] ", join(' ', @order), "\n\n" if DEBUG & 8;
		foreach my $i (@order) {
		    unless ($i) {
			print STDERR " (weird bug encountered)\n" if DEBUG & 32;
			undef @order;
			next;
		    }
		}
	    } until (@order && $order[0]);
	    &save(-1);
	} elsif (-r $playlist) {
	    &load unless $opt_x;
	    system "$ENV{EDITOR} $playlist;clear" if $opt_e;
	}
	exec '$has_mplayer "`cat $playlist`"' if $opt_x;
	#exec "mpg123 --remain --aggressive -@ $playlist" if $opt_x;

	print STDERR BOLD, YELLOW, "binding to $opt_b ...\n", RESET if DEBUG & 16 && $opt_b;
	bind_uniform( $opt_b || $UNI );
	register_uniform();
	$rc = sendmsg ($opt_M, '_notice_summary_play_media',
"[_nick_application]: [_nick] is going to play [_amount_items] items.",
	      { _nick => $nick, _nick_application => 'psycamp',
		_amount_items => $NS } ) if $opt_M;
	print STDERR BOLD, YELLOW, "sent greeting to $opt_M ...\n",
	    RESET if DEBUG & 16 and $opt_M;
	add( \*STDIN, 'r', \&stdread );
	&ginstart;

	print STDERR $ENV{PWD} . " = PWD\n" if DEBUG & 1;
	if ($opt_d) {
		print STDERR BOLD, RED, ($Volatile = $ENV{PWD}
		    =~ /\b$VOLATILE\b/oi) ? <<X : <<Y, RESET;
Warning: ALL files will be deleted after consumption.
X
Warning: Files tagged $VOLATILE will be deleted after consumption.
Y
	}
	if ($opt_D) {
		print STDERR ($Keep = $ENV{PWD} =~ /\b$KEEP\b/oi) ?<<X:<<Y;
Warning: NO files will be deleted.
X
Warning: All files not tagged $KEEP will be deleted after consumption.
Y
	}
	print BOLD, BLACK, &head, <<X, RESET unless $opt_q;
enter (h) for help

X
#%	&gin('channels mono') if $opt_m;    #? X
	$CS = -1;	# global var for current song

	&next(0);
	# ah wait, selecting PCM by command doesn't even work.
	# rgrep USEMASTER in source shows that 'use_master'
	# to toggle PCM doesn't even exist. great docs!  :D
	#&gin('use_master');
	# if we don't want to raise volume to 100% at start then
	# we have to disable the fade-on-next logic as well.
	# let's try to make mplayer use PCM instead of Master.
	&vol( $Volume = $VOL );
	$|=1;

	# Net::PSYC::Event doesn't support idle events yet.. TODO
	add(3, 'i', \&timeout, 1);
	# higher frequency necessary to detect timeouts this way..
	# then again, if it's too high timer is sometimes faster
	# than rxaudio and produces an erroneous kick..
	start_loop();
}


### SUBS & SANDWICHES ###

sub sep { return $_[0] x ($TWIDTH-1) ."\n"; }
sub head {
	my $w = length($TITLE) + length($AUTHOR) +1;
	my $rc = &sep('='). $TITLE;
	$rc .= $TWIDTH > $w ? ' ' x ($TWIDTH-$w)
			    : "\n". ' ' x ($TWIDTH-1-length($AUTHOR));
	return $rc . $AUTHOR ."\n". &sep('-');
}

sub timeout {
    if (!$paused) {
 #{ X
#%	# HACK for rxaudio which sometimes gets enchanted
#%	#y $trick = rand(2)>1 ? 'pause' : 'seek 1 1';
#%	#y $trick = ('pause', 'seek 1 1', 'play')[rand(3)];
#%	#y $trick = 'seek 1 1';
#%#		    print " (kicking rxaudio with '$trick')\n" if DEBUG & 32;
#%#		    &gin( $trick );
#%	print MAGENTA, "\n\t\t(kick) ", RESET if DEBUG & 32;
#%	&gin('seek 1 1'); #? X
#%	&gin('seek 0 1'); #? !X
#%	&gin('pause'); #? !X
 #: X
	print MAGENTA, "\n*** Playback problem. ", RESET, "Skipping $OpenedFile\n";
	# so far timeout only happens when the input file is b0rked, so we skip it
	&next(0);
 #} X
    }
    return 3;
}

sub ginread {
    $_ = <R>;
    print STDERR BOLD, BLUE, $_, RESET if DEBUG & 256;
 #{ X
#%    # example: MSG notify position [offset=20, range=400]
#%    if ( /^MSG notify position / ) {
#%	/\boffset=(\d+), range=(\d+)\b/;
#%	# HACK! HACK!
#%	# send something that will flush the EOF to us
#%	print W "get_player_mode\n" if 5+$1 > $2;
#%    } else
 #} X
    {
	$_ = &ginparse( $_ );
# ds_fill_buffer: EOF reached (stream: audio)
# "EOF code: 2" happens when we ask to load a different file
	if (
#%	    /^MSG notify player state \[EOF\]$/	    #? X
	    /^EOF code: 1\b/			    #? !X
	) {
		# &progress('');
		if ($moveLater) {
		    &moveFile($moveLater);
		    $moveLater = undef;
		} elsif ($deleteLater eq $CurrentFile) {
		    print RED, unlink ($deleteLater) ?
			    "\r***" : "\r - ", BOLD, GREEN, "[\n", RESET;
		    $deleteLater = undef;
		} elsif ($opt_d && ($Volatile ||
		    $CurrentFile =~ /\b$VOLATILE\b/oi)) {
			print RED, unlink ($CurrentFile) ?
			    "\r***" : "\r - ", BOLD, GREEN, "[\n", RESET;
# [could not delete $CurrentFile]
		} elsif ($opt_D && !$Keep &&
		    $CurrentFile !~ /\b$KEEP\b/oi) {
			print RED, unlink ($CurrentFile) ?
			    "\r***" : "\r - ", BOLD, GREEN, "[\n", RESET;
		} elsif ($DUR/1000 - $tc > 4) {
			# Both ffmpeg and mplayer have a bug of
			# dividing the sum of frame bitrates by
			# the duration of the complete track, thus
			# counting zero for the missing part. This
			# formula compensates for that bug.
			my $estira = $I{bitrate} * $DUR/$tc/1000;
			printf "%s%sIncomplete file. Estimated actual bitrate: %.1f%s\n", BOLD, YELLOW, $estira, RESET if $estira < 1000 and $estira > 50;
			# Then again, this approach sometimes
			# fails if mplayer doesn't let us have
			# the actual last timecode of the file -
			# which it unpredictably does.
		} else {
			my $m = $date[$order[$CS]];
			# touch the access time of the file
#	print "\rto be accessed: ", isotime(time),
#		"\tto be changed: ", isotime($m), "\n";
			utime time, $m, $CurrentFile;
 #{ T
#%#	($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
#%#	   $atime,$mtime,$ctime,$blksize,$blocks) = lstat($CurrentFile);
#%#	print "\rset accessed: ", isotime($atime),
#%#		"\tset changed: ", isotime($mtime), "\n";
 #} T
			print BOLD, GREEN, "\r   [\n", RESET unless $opt_q;
		}
		&ginstop if $opt_I;
		&next(0);
	}
    }
}

sub stdread {
    $_ = scalar <STDIN>;
    # should be in timeout instead
    if (waitpid(-1, WNOHANG)) {
	    print BOLD, RED, "\r>>> Media engine has terminated.", RESET, "\n";
	    exit;
    }
    &parse( $_ );
}

sub msg {
    my ($source, $mc, $data, $vars) = @_;
    return if $mc =~ /_circuit/;
    unless ($opt_q) {
	    my $tx = psyctext($data, $vars);
	    print BOLD, YELLOW, <<X, RESET;

>> $source ($mc) >>>>>> $tx <<<<<<

X
    }
    # disable when using -M... hmm, why?
    return if $opt_M;
    &parse($data) if $mc =~
	/^(_request_execute|_command|_message)/;
}

 #{ T
#%sub isotime {
#%        my $t = shift;
#%        return time2str('%Y-%m-%d %T', $t);
#%}
 #} T

sub open {
	my $file = shift;
	return unless $file;
 #{ T
#%	my $m = $date[$order[$CS]];
#%	my $a = $adate[$order[$CS]];
#%	print "\rlast accessed: ", isotime($a),
#%		"\tlast changed: ", isotime($m), "\n";
 #} T
#	&ginopen if $opt_I;
	&ginstart if $opt_I;
	if ($file =~ /\.sdj$/i and -T $file) {
		# calling a script from a script is like a "skip"..
		$SCRIPT = $file;
		print "[executing script $SCRIPT]\n" unless $opt_q;
		open(F, $file) || die "$file: $!";
		while(<F>) {
			&parse($_);
		}
		close F;
		print "[script terminated]\n" unless $opt_q;
		undef $SCRIPT;
		&next(0);		# hmmmm?
	} elsif (! -r $file) {
		my $f = &stringtail($file, $TWIDTH-4);
		print MAGENTA, "*** $f\n", RESET unless $opt_q;
		&next(0);		# hmmmm?
	} else {
		# xaudio does this for us now
#		if (defined( &get_mp3info )) {
#			$mp3 = get_mp3info($file);
#			printf "$file length is %02d:%02d\n",
#			  $mp3->{MM}, $mp3->{SS};
#		}
 #{ X
#%		$TC = '[--:--:--:--]';
#%		&gin("open $file");
 #: X
		$TC = '[--:--:--]';
		&gin("pause");
		# will fail when encountering a filename containing
		# a doublequote. could use urlquote instead.. FIXME
		&gin("loadfile \"$file\"");
		# wicked hacks described in api spec
		&gin("pausing_keep_force pt_step 1");
		&gin("get_property pause");	# ANS_pause=no
 #} X
		$OpenedFile = $file;
		$DUR = 0;
		&sleep(0.3);	# have to wait for file to load?
#		&seek(@_) if @_;
#		&vol(100);
	}
}
sub next {
	my $a = shift;
	&fade(43, $a) if $a and $VOL > 50;
	my $n = $file[$order[++$CS]]; # || $file[$CS];
	&exit(0) if !$n or ($n eq '') or !$NS or $CS >= $NS;
#	return &next(0) if $opt_S and $n =~ /\b$KEEP\b/oi;
	&open( $n );
 #	if (!$a or $opt_I) {
 #		print "(not waiting) " if DEBUG & 32;
#		&sleep(.4);
 #		&gin('play');
 #	}
#% 	&gin('play');	#? X
	if ($opt_M) {
		undef $!;
		$rc = sendmsg ($opt_M, '_notice_play_media_title',
		     "[_nick] is listening to: [_media_title]",
			 { _nick => $nick, _media_title => $n
		 } );
		die "sendmsg $rc: $!" if $!;
		print STDERR BOLD, YELLOW, "sent to $opt_M!\n", RESET if DEBUG & 16;
	}
	&vol($Volume) if $Volume;
	$paused = 0;
	# next;
}

sub parse {
	$_ = shift;
	s/^\s+//;
	chomp;
	if ($SKIP) {
		next unless /^:$SKIP/;
		undef $SKIP;
		next;
	}
	if (/^\?$/) {
		my $any = 0;
		print &sep('='), YELLOW, <<X, RESET;

currently playing: $CurrentFile
X
		my $max = $NS-$CS > 9 ? $CS+9 : $NS-1;
		for ($i = $CS+1; $i <= $max; $i++) {
			my $f = $file[$order[$i]];
			my $s = $size[$order[$i]] || -s $f;
#			my $a = $adate[$order[$i]];
			$f =~ s/\.[^\.\s]+$//i;
			$f = '..'.substr($f,length($p)-$TWIDTH+16) if
				length($f) > $TWIDTH-14;
			$any++;
			printf "%2d.%9d %s\n", $i, $s, $f;
		}
		print $any ? "\n" : "<no more tracks in playlist>\n\n";
		print &sep('-');
		&printinfo;
		next;
	}
	print "> $_\n" if $opt_v and $SCRIPT;

	# techniques to seek in media file
	/^([\d:']+)$/ and &seek($1), next;
	/^(\d+)\s+(\d+)$/ and &seek($1,$2), next;
	/^(g|go|goto|seek)\b\s*(\S*)(.*)$/ and &seek($2,$3), next;
	/^(j|jump)\b\s*(\S*)$/ and &jump($2), next;

	/^skip\s(\w+)$/ and $SKIP=$1, next;
	# /^open\s+(\S*)\s*\b(\S*)\s*\b(\S*)\s*$/ and &open($1,$2,$3), next;
	/^o\s+(.*)\s*$/ and &open($1), next;
	/^(n|next)\b\s*(\S*)$/ and &next($2 || .05), next;
 #{ O
	if ( $ALLOW_DELETE_KEY ) {
	    if ( /^T(T?)\s*$/ ) {
		# 'T' deletes song and remembers it in the index of trash media
		if (open(HATE, ">>", HATEINDEX)) {
			printf HATE "%10d %5s %3s\t%s\r\n",
			   $CurrentSize, $I{duration}, $I{bitrate}, $CurrentFile;
			close HATE;
			print CYAN, ">> marked as trash\n", RESET;
		} else {
			print BOLD, RED, "*** Failed to write to ", HATEINDEX, ":\n", RESET, $out;
		}
		# dirty way to fall through into one of the following ifs
		$_ = $1? 'DD': 'D';
	    }
	    if ( /^(_|DD)\s*$/ ) {
		$moveLater = undef;
		$deleteLater = $CurrentFile;
		print BOLD, BLUE, ">> scheduled for removal\n", RESET;
		next;
	    }
	    if ( /^D\s*$/ ) {
		my $f = $CurrentFile;
		&ginclose;
		print BOLD, RED, ">> deleted: $f\n", RESET if unlink $f;
		&ginopen;
		&next(0);
		next;
	    }
	}
	if ( /^(J|U|K|E|M|V|X|C|F|S|R)(\w?)\s*$/ ) {
		my $r = $2;
		if ($r and $r ne $1) {
			print BOLD, RED, ">> command $1$r not defined\n", RESET;
			next;
		}
		$deleteLater = undef;
		my $t = $1 eq 'J' ? 'DEEJAY' :
			$1 eq 'U' ? 'USE' :
			$1 eq 'K' ? 'KEEP' :
			$1 eq 'E' ? 'EDITABLE' :
			$1 eq 'M' ? 'REMASTER' :
			$1 eq 'V' ? 'VOLATILE' :
			$1 eq 'X' ? 'EXPORT' :
			$1 eq 'C' ? 'CRITICIZE' :
			$1 eq 'F' ? 'FAVES' :
			$1 eq 'S' ? 'SECONDARY' : 'REPERTOIRE';
		my $f = $CurrentFile;
		$f = $ENV{PWD}. '/'. $f unless $f =~ m!^/!;
		my $f2 = $f;
		unless ($f =~ s:\b(SHARE|T|COMPLETE|KEEP|EDITABLE|FAVES|SECONDARY|REPERTOIRE|NEW|SEEK|TODO|USE|DEEJAY|REMASTER|CRITICIZE|INCOMING|EXPORT|VOLATILE|BOMB|FLOW|SPARE|BAR|LISTEN|byArtist|byGenre)\d?\b:$t:i)
		{
			print BOLD, RED, ">> not applicable for $f\n", RESET;
			next;
		}
		if ($f eq $f2) {
			print BOLD, BLUE, ">> no longer scheduled to go to $moveLater\n", RESET if $moveLater;
			$moveLater = undef;
			next;
		}
		if ($r) {
			$moveLater = $f;
			print BOLD, BLUE, ">> scheduled to move to $t\n", RESET;
		} else {
			&moveFile($f);
			&next(0);
		}
		next;
	}
	if ( /^\.\s*$/ ) {
		print BOLD, BLUE, ">> no longer scheduled to go to $moveLater\n", RESET if $moveLater;
		print BOLD, BLUE, ">> no longer scheduled for removal\n", RESET if $deleteLater;
		$moveLater = $deleteLater = undef;
		next;
	}
 #} O
	next unless /^\w/;
	# NEW: relative volume changes, only with mplayer
	# FIXME: why does it only work with leading v?
	/^v\+\s*$/ and &gin('volume 9'), next;	#? !X
	/^v\-\s*$/ and &gin('volume -9'), next;	#? !X
	/^(v|vol|volume)\b\s*(\d+)$/ and &vol($Volume = $2), next;
	/^(f|fade)\b\s*(\S*)(.*)$/ and &fade($2,$3), next;
	/^(r|rise)\b\s*(\S*)(.*)$/ and &rise($2,$3), next;
	/^sleep\b\s*(\S*)$/ and &sleep($1), next;
	/^(d|duration)\b\s*(\S*)$/ and &duration($2), next;
	/^(l|list)\b\s*(\S*)$/ and system("ls $2"), next;
	/^(h|help)\b/ and &help, &printinfo, next;
	/^H\b/ and &help2, &printinfo, next;
	/^(e|edit)\b/ and &edit($CS), next;

	if ( /^\s*(.....+)\s*\b(\S*)\s*\b(\S*)\s*$/ and -r $1 ) {
		&fade(33, .1) if $VOL > 50;
		&open($1, $2, $3);
	#	&sleep(.2);
	#	&gin('play');
	#	&vol(100);
		next;

		# &gin("open $1");
		# &sleep(.4);
		# &sleep;
	}
#	if ( s/^(\S+\.mp3)\s+(\d+)\b// ) {
#		&gin("open $1");
#		&sleep;
#		&gin("seek $2 1000");
#		&sleep;
#		&gin('play');
#	}
#	&gin("open $_") if s!\b(\S+\.mp3)\b!!;
#	if ( s/\b(\d+)\s+(\d+)\b// ) {
#		&gin("seek $1 $2");
#	}

	/^(q|quit)\b/ and &save($CS-1), &exit(0);
	/^(x|exit)\b/ and &exit(0);
	/^(w|write)\b/ and &save($CS-1), next;
	# s/^o\b/open/;
 #{ X
#%	s/^p\b/play/ and $paused = 0;
#%	s/^s\b/stop/ and $paused = 1;
 #: X
	if ( s/^(p|play)\b/pause/ ) {
		next unless $paused;
		$paused = 0;
	}
	if ( s/^(s|stop)\b/pause/ ) {
		next if $paused;
		$paused = 1;
	}
 #} X
	s/^u\b/pause/ and $paused = !$paused;
	&gin($_) if $_;
}

sub moveFile {
	my $f = shift;
	my $d = $f;
	$d =~ s:/[^/]+$::;
	mkpath $d;
#	unless (mkpath($d)) {
#		print ">> could not mkdirhier $d\n";
#		return;
#	}
	unless (rename ($CurrentFile, $f)) {
		print BOLD, RED, ">> could not move file to $d\n", RESET;
		return;
	}
	my $asd = $CurrentFile .".asd";
	if (-s $asd and not rename ($CurrentFile, $f .".asd")) {
		print BOLD, RED, ">> could not move asd file to $d\n", RESET;
		return;
	}
	print BOLD, BLUE, ">> moved to $d\n", RESET;
}

sub sleep {
	my $t = shift;
	if ($t) {
		if ( $t =~ /(\d+)(:|')(\S+)/ ) {
			$t = $1*60+$3;
		}
	}
	else {
		$t = 0.1;
	}
	print "[sleeping $t secs]\n" unless $opt_q or $t < 1;
	select (undef,undef,undef,$t);
	return $t;
}

# stuff being sent to the engine
sub gin {
	my $p = shift;
	print YELLOW, "==> $p\n", RESET if $opt_v;
	print W $p, "\n";
	&sleep(0.1);
}

sub fade {
	my $s = shift;
#%	$s = 33 unless $s;	#? X
	# 12 when using Master
	$s = 3 unless $s;	#? !X
	my $p = shift;
	$p = 0.4 unless $p;
	foreach $i ( reverse @VV ) {
		next if $i >= $VOL;
		last if $i <= $s;
		&vol($i);
		&sleep($p);
	}
	if ($s) {
		&vol($s);
	} else {
		&gin( 'pause' );
		$paused = !$paused;
	}
	&sleep($p);
	return 1;
}
sub rise {
	my $s = shift;
	$s = 100 unless $s;
	my $p = shift;
	$p = 0.2 unless $p;
	&gin( 'play' ) unless $VOL;
	foreach $i ( @VV ) {
		next if $i <= $VOL;
		last if $i >= $s;
		&vol($i);
		&sleep($p);
	}
	if ($s) {
		&vol($s);
		&sleep(0.5);
	}
	$paused = 0;
	return 1;
}

sub vol {
	$VOL = shift;
	# &gin( "volume 100 $VOL 50" );
	#rint W "volume 100 $VOL 50\n";
	&gin( "volume $VOL $VOL 50" );
	#rint W "volume $VOL $VOL 50\n";    # bypasses debug
	return 1;
}

sub seek {
	my $p = shift;
	my $r = shift;

	&duration($r) if $r =~ /[:']/;
	return &jump($p) if $p =~ /[:']/;

	$p = $1 if $p =~ /(\d+)/;
 #{ X
#%	$r = 10 ** length($p) unless ($r);
#%	&gin( "seek $p $r" );
 #: X
	$p = 10 * $p if length($p)==1;
	&gin( "seek $p 1" );
	## $r not supported in mplayer version.. yet?
 #} X
	return 1;
}

# FIXME: duration isn't essential, but annoying not to have
sub duration {
	my $t = shift;

	if ( $t =~ /(\d*)(:|')(\S+)/ ) {
		$t = $1 ? $1*60+$3 : $3;
	}
	$DUR = $t * 1000;
	print YELLOW, "[duration is $t secs]\n", RESET unless $opt_q;
	return 1;
}

sub jump {
	my $t = shift;
	unless ($DUR) {
		print STDERR "[you must specify the song duration first]\n";
		return 1;
	}
	if ( $t =~ /(\d*)(:|')(\S*)/ ) {
		$t = $1 ? $1*60+$3 : $3;
	}
	&gin( "seek ". $t*1000 .' '. $DUR );
	return 1;
}

# if your system doesn't have "which" we're in trouble
# even BSD has which in that place, so it is ok to use full path
sub which {
	my $cmd = shift;
	$_ = `/usr/bin/which $cmd 2>&1`;
	print STDERR "which $cmd: $_" if DEBUG & 1;
	/^(\S+)\s*$/;
	return $1;
}

sub sorttracks {
	my $style = shift;
	return (1 .. $NS) unless $style;
	my @order;
	print STDERR "sorting by_$style 1 .. $NS\n" if DEBUG & 8;
	eval "\@order = sort by_$style 1 .. $NS";
	croak BOLD, RED, <<X, RESET if $@;

Invalid sort option '$style'. Look up '$0 -h' again.

X
	return @order;
}
	# r(andom)	# bad randomizer algorithm (use -r instead)
# this actually produces VERY pseudo random results, says randal
# see http://www.perlmonks.org/?node_id=199901
sub by_r { rand(10) < 5; }
sub by_nr { rand(9) > 3 ? &by_n : &by_r; }
sub by_Nr { rand(9) > 3 ? &by_N : &by_r; }
sub by_cr { rand(9) > 3 ? $a <=> $b : &by_r; }
sub by_sr { rand(9) > 3 ? &by_s : &by_r; }
sub by_Sr { rand(9) > 3 ? &by_S : &by_r; }
sub by_mr { rand(9) > 3 ? &by_m : &by_r; }
sub by_Mr { rand(9) > 3 ? &by_M : &by_r; }
sub by_ar { rand(9) > 3 ? &by_a : &by_r; }
sub by_Ar { rand(9) > 3 ? &by_A : &by_r; }
sub by_n { $file[$a] cmp $file[$b]; }
sub by_N { reverse($file[$a]) cmp reverse($file[$b]); }
sub by_m { $date[$b] <=> $date[$a]; }
sub by_M { $date[$a] <=> $date[$b]; }
sub by_a { $adate[$a] <=> $adate[$b]; }
sub by_A { $adate[$b] <=> $adate[$a]; }
sub by_s {
	# one side of this if/else clause gets optimized away at compilation
	if (DEBUG & 128) {
		my $B = $size[$b]; my $A = $size[$a];
		if ($A == $B) {
			my ($ad, $ai) = stat $a;
			my ($bd, $bi) = stat $b;
			# inform of duplicate files in file system
			# unless they are hard- or softlinked
			print MAGENTA, <<X, RESET
			    unless $ai == $bi and $ad == $bd;
Same file size $B:
	$file[$a] and
	$file[$b]
X
			# we could also be calling cmp, but then we
			# are doing a different job than playing media
		}
		return $A <=> $B;
	} else {
		return $size[$a] <=> $size[$b];
	}
}
sub by_S {
	# one side of this if/else clause gets optimized away at compilation
	if (DEBUG & 128) {
		my $B = $size[$b]; my $A = $size[$a];
		print MAGENTA, <<X, RESET if $A == $B;
Same file size $B:
	$file[$a] and
	$file[$b]
X
		return $B <=> $A;
	} else {
		return $size[$b] <=> $size[$a];
	}
}

# FIXME: maybe update to use List::Util 'shuffle';
# or maybe this is fine because it does NOT use sort+rand
sub randomize {
	my @tmp = 1 .. $NS;
	my @order = ();
	my $lr = -99;
	for my $j (1 .. $NS) {
		my $r = int(rand($#tmp));
		my $ir = $tmp[$r];

		# this tries to avoid items being too near
		# even if randomizer suggests so -
		# in particular try not to play the same artist
		# in a row, that's why we look at the filename
		if ($#tmp>7 and abs($lr-$ir)<23 and
		   substr($file[$lr],0,PATHMATCH) eq
		   substr($file[$ir],0,PATHMATCH)) {
			print STDERR <<X if DEBUG & 4;
last=$lr\t[$file[$lr]]
near=$ir\t[$file[$ir]] ($r)
X
			$r = int(rand($#tmp));
			$ir = $tmp[$r];
			if (substr($file[$lr],0,PATHMATCH) eq
			    substr($file[$ir],0,PATHMATCH)) {
				print STDERR <<X if DEBUG & 4;
new =$ir\t[$file[$ir]] ($r)
\t\tno good, one more try:
X
				$r = int(rand($#tmp));
				$ir = $tmp[$r];
			}
			print STDERR <<X if DEBUG & 4;
new =$ir\t[$file[$ir]] ($r)

X
		}
		$lr = splice @tmp, $r, 1;
		push @order, $lr;
	}
	return @order;
}

sub enqueue {
	my $saveNS = $NS;
	for $_ (@_) {
		if (-d $_ && -x _ && -r _) {
			print STDERR "\n[finddepth: $_]\n" if DEBUG & 2;
			&File::Find::finddepth(\&wanted, $_);
			next;   # $_ is corrupted after finddepth
		}
		$name = $_;
		print STDERR "\n[file wanted: $_]\n" if DEBUG & 2;
		&wanted;
	}
	if (my $new = $NS - $saveNS) {
	       &progress($new, ' tracks found');
	       print "\n";
	}
}
sub wanted {
	# 2018: I changed 'lstat' to 'stat' here so that symlinks
	# are processed properly. Does it have any side effects?
	#
	($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
	   $atime,$mtime,$ctime,$blksize,$blocks) = stat($_);
	my $neat = &stringtail($name, $TWIDTH-7) unless $opt_q;
	if (-f _ && -r _ && -s _ > 9999 && /\.$FILETYPES$/io) {
		$file[++$NS] = $name;
		$size[$NS] = $size || -s _;
		$date[$NS] = $mtime || -M _;
		$adate[$NS] = $atime;
		&progress('yes: ', $neat);
	} elsif ( -d _ && -e "$_/.prune" ) {
		$prune = 1;
		&progress('pruned: ', $name);
		print "\n";
	} else {
		&progress('no : ', $neat) if -f _;
	}
}
sub progress {
	return if $opt_q;
	my $tx = join('', @_);
	my $len = length $tx;
	print "\r", GREEN, $tx, RESET, ' ';
	print '_' x ($TWIDTH-2-$len), ' ' if $len < $TWIDTH-1;
	print "\n" if DEBUG & 4096;
}
sub printinfo {
	&progress($CurrentInfo) unless $opt_v;
	print GREEN, $CurrentInfo, RESET, "\n" if $opt_v;
}
sub stringtail {
	my $s = shift;
	my $w = shift || $TWIDTH;
	return length($s)>=$w ? "...".substr($s,-$w+4) : $s;
}

sub ginxpect {
	local($match) = @_; 

	while(<R>) {
		last if /$match/i;      # not /o!
		&ginparse($_);
	}
	# print "\n";
}
sub ginparse {
	# most frequent events first!
	if (
#%	    /^MSG notify timecode (\S+)/    #? X
	    /^A:\s*([\d]+\.\d)\s/	    #? !X
	) {
		unless (
#%		    $TC eq $1   #? X
		    $tc == $1	#? !X
		) {
			&seek(84) if $opt_j and $tc == 8;
#%			$TC = $1;			#? X
			$tc = $1;			#? !X
			$TC = time2str('[%T]', $tc, 0);	#? !X
			print BOLD, GREEN, "\r$TC ", RESET unless $opt_q;
			# $TC then gets used all over the place...
		}
		return 0;
	}
	if ($opt_V or DEBUG & 512) {
 #{ !X
		return if / supported but disabled$/;
		return if /^(Configuration: --|CommandLine:|get_path)/;
 #} !X
		print STDERR BOLD, BLUE, $_, RESET;
	}
 #{ X
#%	if (/^MSG notify duration \[(\d+)\]/) {
#%		# kludge to get around a bug in xaudio (duration output twice)
#%		if ($1 != $LDUR) {
#%			$LDUR = $DUR = $1;
#%			$DUR *= 1000 if $DUR < 1000;
#%			$CDUR += $DUR;	# cumulative duration
#%		}
#%		return 0;
#%	} 
#%	if (/ stream info \[(.+)\]/) {
#%		%I = split /[=,\s]+/, $1;
#%		my $d = $DUR / 1000;
#%		$I{duration} = sprintf("%02d:%02d", $d / 60, $d % 60);
#%#		if ($opt_l) {
#%#			my $f = $file[$order[$CS]];
#%#			my $s = $CurrentFile eq $f ? -$size[$order[$CS]] : -s $f;
#%#			printf ("%9d %5s %3s\t%s\n", $s,
#%#				$I{duration}, $I{bitrate}, $CurrentFile);
#%#		}
#%		$I{mode} = lc $I{mode};
#%		if ($opt_c) {
#%			my $d = $CDUR / 1000;
#%			$I{cumulative} = sprintf("%02d:%02d", $d / 60, $d % 60);
#%			&progress("$TC   -> [$I{duration}] {$I{cumulative}} $I{bitrate} mp$I{layer}.$I{level} $I{frequency} $I{mode} ");
#%		} else {
#%			&progress("$TC   -> [$I{duration}] mp$I{layer}.$I{level} with $I{frequency} Hz at $I{bitrate} kbps in $I{mode}");
#%		}
#%		return $_;
#%	}
#%#	if ($opt_l and / ack \[XA_MSG_COMMAND_INPUT_OPEN\]/) {
#%#		my $f = $file[$order[$CS]];
#%#		my $s = $CurrentFile eq $f ? -$size[$order[$CS]] : -s $f;
#%#		printf ("%9d\t%s\n", $s, $CurrentFile);
#%#	}
 #: X
	if ( /^\[file\] File size is (\d+) bytes$/ ) {
	    $size = $1;
	    print STDERR "Got size: $size\n" if DEBUG & 512;
	    return 0;
	}
# [lavf] stream 0: audio (opus), -aid 0, -alang eng
# INFO: libavcodec "aac" init OK!
# Selected audio codec: [ffaac] afm: ffmpeg (FFmpeg AAC (MPEG-2/MPEG-4 Audio))
	if ( /^\[lavf\] .* audio \((\S+)\),/ ) {
	    $codec = $1;
	    print STDERR "Got codec: $codec\n" if DEBUG & 512;
	    return 0;
	}
 #} X
	if (
#%	    / input name \[(.+)\]/		#? X
#	    /^Playing (.+)\.$/			#? !X
	    /^STREAM: \[file\] (.+)$/		#? !X
	) {
		# global vars for current filename
		my $nf = $1;
		# we do indeed not receive a proper info for mp2, mp3...
		$CurrentCodec = $codec;
		$CurrentSize = $size || -s $nf;
		if ($CurrentFile ne $nf) {
			$moveLater = undef;
			$deleteLater = undef;
			$size = undef;
			$codec = undef;
		}
		$CurrentFile = $nf;
		return $_;
	}
 #{ X
#%	#return 0 if /^(play|close|open|volume)/;
#%	return 0 if /^(play|volume|get_player_mode)/;
#%	return $_ if $opt_q or / notify (position|ack|play|state|can seek)/;
#%	if ( /^MSG notify debug \[.* message=\"no audio device found\"\]/ ) {
#%		print "\n\r", <<X;
#%*** No audio device accessible. Try modprobe snd-pcm-oss!
#%X
#%		&exit(-1);
#%	}
#%	if (DEBUG & 2048 and /^MSG notify debug \[.* message=\"(.+)\"\]/) {
#%		print "\r*** ", $1, "\n";
#%		return 0;
#%	}
#%	return $_ if !$opt_v && /notify (debug|output|input|nack)/;
#%	# my $o = $_;
#%	s/^MSG notify //i;
#%	print BOLD, MAGENTA, "\r*** ", $_, RESET;
 #: X
	if ( /^AUDIO: (\d+) Hz, (\d+) ch, \S+ (\d+)\.\d kbit/ ) {
		%I = undef;
		$I{frequency} = $1;
		$I{channels} = $2;
		$I{bitrate} = $3;
		return 0;
	}
	if ( /^Starting playback/ ) {
		if ( (!$I{duration} || !$I{bitrate}) and $has_ffmpeg and
		  # while mplayer is starting to play the track
		  # we fetch further information on it...
		  (my $pid = open3(my $FFW, my $FF, my $FFE,
		  $has_ffmpeg, '-i', $CurrentFile))) {
		    while(<$FF>) {
			# quite ridiculous that we have to also run
			# an ffmpeg to obtain track duration which
			# mplayer knows but does not show.
			if (/\bDuration: (\d\d):(\d\d):(\d\d)\.\d\d.+ bitrate: (\d\d+) kb/ and $1 ne '00:00:00') {
			    my $hh = $1;
			    my $mm = $2;
			    my $ss = $3;
			    my $br = $4;
			    # unfortunately the bitrate we got from
			    # mplayer frequently just uses some frame
			    # from the track, not producing a median
			    # value for VBR tracks. amazing how people
			    # still don't dig VBR after 20 years.
			    print MAGENTA, "\rInconsistent bitrate info: ", RESET,
			      "$I{bitrate} vs $br\n" if $opt_v and $br
			      and $I{bitrate} and abs($br-$I{bitrate}) > 1;
			    $I{bitrate} = $br if $br;
			    $I{duration} = $hh eq '00' ? "$mm:$ss" : "$hh:$mm:$ss";
			    $DUR = (60*60*$hh + 60*$mm + $ss) * 1000;
			    $CDUR += $DUR;	# cumulative duration
			    break;
			}
		    }
		    close $FF;
		    close $FFW;
		    close $FFE;
		    waitpid( $pid, 0 );
		    print RED, "\r$has_ffmpeg died with status ", $? >> 8, RESET, "\n"
			if $? != 256;
		}
		my $nf = $CurrentFile;
		$nf =~ s/\.([^\.]+)$//i;
		$codec = lc($1) unless $codec;
		my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
		       $atime,$mtime,$ctime,$blksize,$blocks)
		    = stat($CurrentFile);
		$PrintCurrentFile = $nlink>1?
		  YELLOW. $CurrentSize .BLUE.'#'. $nlink .RESET:
		  YELLOW. $CurrentSize .RESET;
		# consider color codes:
		my $twidth = $TWIDTH + length(YELLOW) + length(RESET);
		$twidth += length(BLUE) if $nlink>1;
		$PrintCurrentFile = $codec?
		  "<$codec> $PrintCurrentFile ": $PrintCurrentFile .' ';
		$PrintCurrentFile .= &stringtail($nf, $twidth
					-length($PrintCurrentFile) );
		print "\r$PrintCurrentFile\n";
		$CurrentInfo = "$TC   -> [$I{duration}] $I{frequency} Hz at $I{bitrate} kbps with $I{channels} channels";
		&printinfo;
		return 0;
	}
 #} X
	return $_;
}

sub ginstart {
	return if $GINPID;
	if ($opt_I) {
		system("soundoff");
		system("soundon");
		system("clear");
	}
	$R = new FileHandle; $R->autoflush;
	$W = new FileHandle; $W->autoflush;
 #{ X
#%	$GINPID = open3( \*W, \*R, \*R, "$wrapper $has_rxaudio");
#%	$GINPID || die "$wrapper $has_rxaudio: $!";
#%	&ginxpect('ready');
 #: X
	# -v is needed to receive the EOF!
	# '-osdlevel', '3' can be useful
	# to allow for fading we want to have the PCM channel,
	# this works for ALSA - does it fail otherwise?
	$GINPID = open3( \*W, \*R, \*R, $has_mplayer, '-slave', '-idle', '-v', '-fs', '-zoom', '-screen', $opt_S || $screen, '-mixer-channel', 'PCM' );
#	my $mpvsock = '/tmp/.mpvsock';
#	$GINPID = open3( \*WW, \*R, \*R, $has_mpv, "--input-ipc-server=$mpvsock", '--idle=yes', '-v', '--fs=yes', '--no-audio-display', "--screen=".($opt_S || $screen), '--alsa-mixer-name=PCM' );
#	#open( \*R, '<', $mpvsock );
#	open( \*W, '>', $mpvsock );
	R->blocking(0);
	$GINPID || die "$has_mplayer: $!";
	&ginxpect('^MPlayer');
 #} X
	print STDERR <<X if DEBUG & 64;
audio engine running as $GINPID
X
	add( \*R, 'r', \&ginread );
	$output_open = 1;
}
sub ginstop {
	remove( \*R );
 #{ X
#%	gin('exit');
 #: X
	gin('quit');
 #} X
	close R;
	waitpid( $GINPID, 0 );
	undef $GINPID;
	$output_open = 0;
}
sub ginclose {
#%	gin('output_drain');  #? X
	$output_open = 0;
#%	gin('output_close');  #? X
}
sub ginopen {
	return if $output_open;
	if ($opt_I) {
		system("soundoff");
		system("soundon");
		system("clear");
	}
#%	gin('output_open');   #? X
	$output_open = 1;
}

sub exit {
	&ginstop;
        if ($CDUR and not $opt_q) {
		$CDUR /= 1000;
                my $SS = int $CDUR % 60;
                my $M2 = int $CDUR / 60;
                my $HH = int $M2 / 60;
                my $MM = int $M2 % 60;
                printf "\r%s%sTOTAL\t%5d:%.2d  (= %.2d:%.2d:%.2d)%s\n",
                    BOLD, BLACK, $M2,$SS, $HH,$MM,$SS, RESET;
        }
	exit(shift);
}

sub load {
	my $tracks = $opt_L? 'tracks': 'entries';
	local *L;
	return unless open (L, $playlist);
	for ($NS=0; <L>;) {
		chomp;
		if ($opt_L and not -r $_) {
			print "Skipping $_\n";
			next;
		}
		$order[$NS] = $NS;
		$file[$NS++] = $_;
	}
	close L;
	print YELLOW, "\rLoaded $NS $tracks from $playlist \n\n", RESET
	    unless $opt_q;
}
sub save {
	my $cs = shift;
	local *L;
	umask 077;
	return unless open (L, ">$playlist");
	print YELLOW, "\rSaving playlist into $playlist\n\n", RESET
	    unless $opt_q;
	foreach my $i ($cs+1 .. $NS-1) {
		my $f = $file[$order[$i]];
		print L $ENV{PWD}, '/' unless $f =~ m!^/!;
		print L $f, "\n";
	}
	close L;
}
sub edit {
	&save(shift);
	system "$ENV{EDITOR} $playlist;clear";
	&load;
	$CS = -1;
	print "$PrintCurrentFile\n";
	&printinfo;
}

sub help { print BOLD, BLACK, &head, RESET, <<X, &sep('='); }

basics:	(q)uit (h)elp

motion:	(p)lay (s)top pa(u)se
      [ (j)ump ] <mm:ss> 		jump to an absolute point in the song
      [ (g)oto ] <pos> [<range>]	 can do smart guessing of range value
 (for example you can simply type '0' thru '9' to jump to a point in the song)

files:	(o)pen <file>			       immediately load this new song
	<file>		    a filename by itself will first fade current song
	(l)ist [<dir>]					     simply calls 'ls'
	(n)ext					      next file from playlist
	'?'			show a list of the next 9 tracks in the queue
	(w)rite or (e)dit playlist
	e(x)it					exit without updating playlist

volume:	(v)olume [0..100]			    default is maximum volume
	v+  v-				    increase or decrease volume a bit
	(f)ade [<volume> [<psecs>]]	     psecs: time between volume steps
	(r)ise [<volume> [<psecs>]]		       (example: fade 33 0.1)

extra commands for scripting:
	sleep <time>	wait for <time> before executing next command

type (H)elp for organizing commands
X

 #{ O
# when organizing mode has been activated in psycamp, you can reorganize
# your media files as follows:
sub help2 { print BOLD, BLACK, &head, RESET, <<X, &sep('='); }

Whenever media is in a directory like INCOMING, NEW or TODO, it can be
moved into a different subdirectory on the same hierarchy level by using
the following uppercase commands:

	J = send to DEEJAY folder
	F = send to FAVES
	K = send to KEEP
	X = send to EXPORT
	S = send to SECONDARY
	R = send to REPERTOIRE
	V = send to VOLATILE

If you prefer to execute the command *after* having finished playing it,
you can schedule the move for later by doubling the command letter. So
you type 'KK' if you want to keep the file after having listened to it.
You can change your mind while the track is playing - the last command
you schedule is the one that counts. You can cancel a scheduled operation
using '.' (as in 'leave it here'). The commands for deletion are similar:

	D = delete the file now
	DD = delete the file after having played it
	T = delete the file and mark as trash
	TT = mark as trash and delete later

Consider also the -d and -D options which delete files automatically
after consumption depending on the name of the directory.

type (h)elp for regular commands
X
 #: O
#%sub help2 { print RED, "\r", <<X, RESET, "\n"; }
#%Sorry, organizing functions have been disabled.
#%X
 #} O

__END__

=pod

=head1 NAME

 psycamp - command line media player with PSYC remote control

=head1 SYNOPSIS

 psycamp [<flags>] [-b <uniform>] [-n <nick>] [-s <sort-algorithm>]
	 [-M <UNI>] [-S <screen>] [<media-files|directories>]

	[-b]ind PSYC uniform and accept commands from both PSYC and stdin
	[-M] sends currently playing title to a monitoring entity via PSYC
	[-n]ickname to use for monitoring, otherwise a default will be used
	[-s]ort playlist according to one of the algorithms explained below
	[-S]creen number to display videos on (default: 0)

 Flags:
	[-H] shows an explanation what this tool is good for, try it!
	[-r]andomize using a smart shuffle algorithm, much better than "-s r"
	[-v]erbose: shows some extra output
	[-q]uiet: shows close to no output
	[-c]alculate cumulative duration of selections
	[-L]oad the tracks in the playlist only if they really exist
	[-x] will terminate perl and exec mplayer, use when short on memory
	[-j]ump to end of track: a way to scan through a playlist
	[-d]elete files after playing if the path contains the word '$VOLATILE'.
	[-D]elete files after playing unless the path contains the word '$KEEP'.

 Without arguments, psycamp resumes from last run's playlist.

=head2 Sort algorithms:

	n(ame)		# sorts by file path (directory first)
	N(ame)		# sorts by file ending (reverse of -n)
	s(ize)		# hear silly small sound snippets first
	S(ize)		# hear big epic pieces first
	m(odification)	# hear newest tracks first
	M(odification)	# hear oldest tracks first
	a(ccessTime)	# hear tracks you haven't heard in a long time first
	A(ccessTime)	# hear tracks you recently accessed first
        cr		# gives the order given on commandline a slight shuffle

 You can append 'r' to each algorithm (as in 'nr' or 'Ar') to apply a slight 
 random shuffle of the items while roughly following the sorting principle.

=head1 DESCRIPTION

This has been around as 'psycmp3', but since 2017 it can also
play other formats.. so I shalt rename it to 'psycamp', the
psyc amplifier, or the psyca media player...  :)

This media player is over a decade old, but it still is my tool of
choice. I gave it functions i didn't find in any other..  How
useful is a media player that can't easily reorganize or at least 
delete files you don't want to consume ever again?

For further help on how to actually use the beast, type 'h' and
ENTER once it is playing some media.

=head2 PSYC Remote Control

You can use the 'psyccmd' script to remote control psycamp which 
therefore can act as a music jukebox or media player daemon. You
could be generating those PSYC messages from whichever other
tool you find appropriate. The format is easy. You may need to
use '-b' to bind to an accessible network interface.

=head2 PSYC Notification

psycamp can obviously generate 'playing now' notifications. Just
provide '-M' and '-b' accordingly.

=head1 CAVEATS

psycamp uses a line mode interface. Each command needs to be
submitted to the program by hitting the ENTER key.
It's unusual, but not a problem.

psycamp ignores so-called ID3. it expects tracks to have meaningful
file and directory names instead, since that is where you need the 
information when shuffling files around.

When watching video with psycamp, keystrokes in the video frame
will go directly to mplayer, bypassing psycamp. This is fine for
several interactions but psycamp gets stuck if you quit mplayer.

=head1 AUTHORS

carlo von lynX.

=head1 COPYRIGHT

This program is free software, published under the Affero GNU Public
License. A disclaimer isn't necessary in my country, nor do I need
to mention the current year to assert a copyright.
