#!/usr/bin/perl -w
#
# Name: sshmpg
#
# Author: Luke Scharf (luke@vt.edu), Feb 2001
#
# Purpose: To play and cache mp3's from the file system of an ssh-server.
#
# Dependencies: gtk-perl, perl-ipc, ssh, scp, du, ls, and xmms.
# 
# Reference: http://personal.riverusers.com/~swilhelm/gtkperl-tutorial/
#	(Starting point for this iteration is the Tree Widget example program)
#
# License: GPL ( see http://www.gnu.org/copyleft/gpl.html )
#
# Fixed bug in keystroke-handling  Shane
# And playlist
# 
############## Pre ##############
# Directives
use Gtk;
use IPC::Open2;
use strict;

# Set up toolkits
init Gtk;
set_locale Gtk;

# Constants
my $false = 0;
my $true = 1;

# Global Variables
my $root_dir = "/";
my @titles;
my $REMOTE_IN;
my $REMOTE_OUT;
my $list_row = -1;

# Global Widgts
my $window;
my $entry;
my $list;

# Global Varsiables - Remote Server Options
my $PathPrefix	= "/";
my $Shell	= "/bin/bash";

# Global Variables - Local Client Options
my $Hostname	= "localhost";
my $Username	= $ENV{"USER"};
my $Cachedir	= "/tmp/music_cache";
my $Cachesize	= 150000;
my $Cachekill	= 5;
my $Timeout	= 20;
my $TimeRefresh	= 60;


############## Config ##############
# Get CLI options

# Read config file
read_localconfig();

# Read Command Line
if ( $ARGV[0] )
{
	$Hostname = $ARGV[0];
}
else
{
	printf("Usage: sshmpg <hostname> <username>\n");
	exit;
}

if ( $ARGV[1] )
{
	$Username = $ARGV[1];
}


# Setup cache
system("mkdir $Cachedir &> /dev/null");

# Open connection
remote_open();
remote_readconfig();

# Construct Window
build_window();

# Main
main Gtk;

# Close connection
remote_close();

# We should never get here
exit( 0 );



############## Subroutines ##############

# Misc
sub shellify
# Make a filename parseable by /bin/bash.
{
	my ( $filename ) = @_;

	$filename =~ s|//|/|g;
	$filename =~ s|\ |\\\ |g;
	$filename =~ s|'|\\'|g;
	$filename =~ s|&|\\&|g;
	$filename =~ s|\(|\\\(|g;
	$filename =~ s|\)|\\\)|g;

	return $filename;
}


sub read_localconfig
# Read the local configuration file.
# Results are written to global variables.
{
	open(CONFIGFILE, "< $ENV{'HOME'}/.sshmpg");
	
	# Parse
	while ( <CONFIGFILE> )
	{
		if ( $_ =~ "Cachedir" )
		{
			@_ = split();
			$Cachedir = $_[1];
		}

		if ( $_ =~ "Cachesize" )
		{
			@_ = split();
			$Cachesize = $_[1];
		}

		if ( $_ =~ "Cachekill" )
		{
			@_ = split();
			$Cachekill = $_[1];
		}
		
		if ( $_ =~ "Timeout" )
		{
			@_ = split();
			$Timeout = $_[1];
		}
		
		if ( $_ =~ "TimeRefresh" )
		{
			@_ = split();
			$TimeRefresh = $_[1];
		}
	}
}


# Remote Control
sub remote_open
# Open a connection to the remote machine.
{
	my $pid = open2(*REMOTE_IN, *REMOTE_OUT, "ssh -o \"BatchMode yes\" -l $Username $Hostname $Shell" )
		or die "Could not execut ssh";
}


sub remote_close
# Close the remote connection
{
	printf(REMOTE_OUT "exit\n");
	close(REMOTE_IN);
	close(REMOTE_OUT);
}


sub remote_readconfig
# Read the config file on the remote machine.
# Write results to global variables
{
	# Send command
	printf(REMOTE_OUT "cat /etc/sshmpg.server ; echo \\\*END\n");
	
	# Parse
	$_ = <REMOTE_IN>;
	while ( $_ and not ($_ =~ /\*END/) )
	{
		if ( $_ =~ "PathPrefix" )
		{
			@_ = split();
			$PathPrefix = $_[1];
		}

		if ( $_ =~ "Shell" )
		{
			@_ = split();
			$Shell = $_[1];
		}

		# Move on
		$_ = <REMOTE_IN>;
	 }
}


sub remote_dirs
# Get a list of directories in the specified directory on the remote machine.
# Return as an array.
{
	# Parameters
	my ( $remotepath ) = @_;

	# Vars
	my @retval;

	# Get the contents of the directory
	printf(REMOTE_OUT "ls -LF \"$remotepath\" ; echo \\\*END\n");

	# Parse
	$_ = <REMOTE_IN>;
	while ( $_ and not ($_ =~ /\*END/) )
	{
		if ($_ =~ /\//)
		{
			# Get name if the subdir
			chop;
			chop;
			@_ = split(/\//, $_);

			# Store
			$retval[++$#retval] = $_[$#_];
		}

		# Move on
		$_ = <REMOTE_IN>;
	 }

	# Return data
	 return @retval;
}


sub remote_files
# Get a list of files in the specified directory on the remote machine.
# return as an array
{
	# Parameters
	my ( $remotepath ) = @_;

	# Vars
	my @retval;

	# Get the contents of the directory
	printf(REMOTE_OUT "ls -LF \"$remotepath\" ; echo \\\*END\n");

	# Parse
	$_ = <REMOTE_IN>;
	while ( $_ and not ($_ =~ /\*END/) )
	{
		if (not ($_ =~ /\//))
		{
			# Get name if the subdir
			chop;
			if ( ($_ =~ /\*/) or ($_ =~ /\@/) )
			{
				chop;
			}
			@_ = split(/\//, $_);

			# Store
			$retval[++$#retval] = $_[$#_];
		}

		# move on
		$_ = <REMOTE_IN>;
	 }

	 return @retval;
}



# Cache
sub cache_hash
# Figure out a unique name for the local filename
{
	my ( $name ) = @_;

	$name =~ s|//|/|g;
	$name =~ s|[/ ]|_|g;

	return $name;
}


sub cache_add
# Add a remote file into the cache
{
	my ( $remotefile ) = @_;

	# Variables
	my $localfile;
	my $localfile_shell;
	my $command;
	my $path;
	my $seconds;

	# Is the cache full?
	cache_trim();

	# Is the file already cached?
	if ( cache_query( $remotefile ) )
	{
		return;
	}

	# Compose localfile
	$localfile = cache_hash("$Hostname/$remotefile");
	$localfile = "$Cachedir/$localfile";
	$localfile_shell = shellify( "$localfile" );

	# Fix up remotefile
	$remotefile = shellify( $remotefile );

	# Compose Command
	$command = "scp $Username\@$Hostname:\"$remotefile\" $localfile_shell &";

	# Call command
	system($command);

	# Wait for transfer to start
	$seconds = 0;
	while ( ($seconds < $Timeout ) and not ( -f  $localfile ) )
	{
		sleep(1);
		$seconds++;
	}

	# Refresh display
	$path = $entry->get_text();
	show_files( "$path" );
	$list->moveto( $list_row, 0, 0.50, 0.0);
	$list->select_row( $list_row, 0);
}


sub cache_trim
# Cut down the size of the cache
{
	# Variables
	my $my_cachesize;
	my $targetfile;
	my $i;

	# Trim
	for($i=0; $i < $Cachekill; $i++)
	{

		# Get size
		open(CACHE, "du -s $Cachedir |") or die "Cound not du -s $Cachedir";
		$_ = <CACHE>;
		@_ = split();
		$my_cachesize = $_[0];
		close(CACHE);

		if ( $my_cachesize > $Cachesize )
		{
			# Get name of file to delete
			open(OLDEST, "ls -U $Cachedir |") or die "Cound not ls -u $Cachedir";
			$_ = <OLDEST>;
			@_ = split();
			$targetfile = $_[0];
			close(OLDEST);
			$targetfile = shellify( $targetfile );

			# Delete file
			printf("Uncache: $Cachedir/$targetfile\n");
			system("rm $Cachedir/$targetfile\n");
		}
	}

}


sub cache_query
# Is this file cached?
{
	my ( $remotefile ) = @_;

	# Variables
	my $localfile;

	# Build localfile name
	$localfile = cache_hash( "$Hostname/$remotefile" );
	
	if( -f  "$Cachedir/$localfile" )
	{
		return $true;
	}
	
	return $false;
}



# GUI
sub build_window
{
	# Local variables
	#my $window
	my $pane;
	my $vbox_left;
		#my $entry;
		my $tree_scrolled_win;
			my $tree;
			my $leaf;
			my $subtree;
	my $vbox_right;
		my $bbox;
			my $play_button;
			my $enqueue_button;
			my $refresh_button;
			my $encache_button;
			my $quit_button;
		my $list_scrolled_win;
			#my $list;
			my $item;
	my $timer;

	# Construct Window
	$window = new Gtk::Window( 'toplevel' );
	$window->set_usize( 725, 500 );
	$window->set_title( "sshmpg - $Username\@$Hostname" );
	$window->signal_connect( "delete_event", sub { Gtk->exit( 0 ); } );
       
	$pane = new Gtk::HPaned();
	$window->add( $pane );
	$pane->set_handle_size( 10 );
	$pane->set_gutter_size( 8 );
	$pane->show();

	# Create a VBox for the left pane - Entry and Tree Scrolled Window
	$vbox_left = new Gtk::VBox( $false, 0 );
	$pane->add1( $vbox_left );
	$vbox_left->show();

	# Create a ScrolledWindow for the tree
	$tree_scrolled_win = new Gtk::ScrolledWindow( undef, undef );
	$tree_scrolled_win->set_usize( 150, 400 );
	$vbox_left->pack_start( $tree_scrolled_win, $true, $true, 0 );
	$tree_scrolled_win->set_policy( 'automatic', 'automatic' );
	$tree_scrolled_win->show();

	# Create the Text Entry
	$entry = new Gtk::Entry();
	$vbox_left->pack_start( $entry, $false, $false, 4 );
	$entry->signal_connect( 'activate', \&entry_activate );
	$entry->show();

	# Create a VBox for the right pane - buttons and file list
	$vbox_right = new Gtk::VBox( $false, 0 );
	$pane->add2( $vbox_right);
	$vbox_right->show();

	# Create a ScrolledWindow for the list
	$list_scrolled_win = new Gtk::ScrolledWindow( undef, undef );
	$vbox_right->pack_start( $list_scrolled_win, $true, $true, 0);
	$list_scrolled_win->set_policy( 'automatic', 'automatic' );
	$list_scrolled_win->show();

	# Create list box
	@titles = qw( Filename Status );
	$list = new_with_titles Gtk::CList( @titles );
	$list_scrolled_win->add( $list );
	$list->set_column_width( 0, 480 );
	$list->set_column_width( 1, 20 );
	$list->set_selection_mode( 'single' );
	$list->set_shadow_type( 'none' );
	$list->show();
	$list->signal_connect( 'select_row', \&select_callback);
	$list->signal_connect( 'unselect_row', \&unselect_callback);

	# Create buttonbox to to go above the ScrolledWindow
	$bbox = new Gtk::HButtonBox();
	#$bbox->set_layout_default( 'end' );
	$bbox->border_width( 5 );
	$bbox->show();
	$vbox_right->pack_end( $bbox, $false, $false, 0);

	# Create Play button
	$play_button = new Gtk::Button( "Play" );
	$play_button->show();
	$bbox->add($play_button);
	$play_button->signal_connect( 'clicked',
		\&play_button_callback, $list );

	# Create Enqueue button
	$enqueue_button= new Gtk::Button( "Enqueue" );
	$enqueue_button->show();
	$bbox->add($enqueue_button);
	$enqueue_button->signal_connect( 'clicked',
		\&enqueue_button_callback, $list );

	# Create Refresh button
	$refresh_button= new Gtk::Button( "Refresh" );
	$refresh_button->show();
	$bbox->add($refresh_button);
	$refresh_button->signal_connect( 'clicked', \&refresh_callback);

	# Create Enchache button
	$encache_button = new Gtk::Button( "Encache" );
	$encache_button->show();
	$bbox->add($encache_button);
	$encache_button->signal_connect( 'clicked',
		\&encache_button_callback, $list );
	
	# Create Quit button
	$quit_button = new Gtk::Button( "Quit" );
	$quit_button->show();
	$bbox->add($quit_button);
	$quit_button->signal_connect( 'clicked',
		\&quit_button_callback );

	# Create root tree
	$tree = new Gtk::Tree();
	$tree_scrolled_win->add_with_viewport( $tree );
	$tree->set_selection_mode( 'single' );
	$tree->set_view_mode( 'item' );
	$tree->show();

	# Create root tree item widget
	$leaf = new_with_label Gtk::TreeItem( $root_dir );
	$tree->append( $leaf );
	$leaf->signal_connect( 'select', \&select_item, $root_dir );
	$leaf->set_user_data( $root_dir );
	$leaf->show();

	# Create timer
	$timer = Gtk->timeout_add( 1000*$TimeRefresh, \&refresh_callback);

	# Create the initial subtree
	if ( remote_dirs( "$PathPrefix/$root_dir" ) )
	{
		$subtree = new Gtk::Tree();
		$leaf->set_subtree( $subtree );
		$leaf->signal_connect( 'expand', \&expand_tree, $subtree );
		$leaf->signal_connect( 'collapse', \&collapse_tree );
		$leaf->expand();
	}

	# Finish
	$window->show();
}


sub window_cursor_watch
{
	my $cursor = new Gtk::Gdk::Cursor( '150' );
	$window->window->set_cursor( $cursor );
	while (events_pending Gtk)
	{
		main_iteration Gtk;
	}
}


sub window_cursor_arrow
{
	# Set cursor
	my $cursor = new Gtk::Gdk::Cursor( '68' );
	$window->window->set_cursor( $cursor );
	while (events_pending Gtk)
	{
		main_iteration Gtk;
	}
}


# Callbacks
sub expand_tree
# Callback for expanding a tree - find subdirectories and add them to tree
{
	# Variables
	my ( $item, $subtree ) = @_;
	my $dir;
	my @dir_entries;
	my $dir_entry;
	my $path;
	my $item_new;
	my $new_subtree;
	$dir = $item->get_user_data();

	# Ask for a filelist
	@dir_entries = remote_dirs("$PathPrefix/$dir");

	# Read and process filelist
	foreach $dir_entry  (@dir_entries)
	{
		# Add the subdir into the tree
		$path = $dir . "/" . $dir_entry;
		$path =~ s|//|/|g;
		$item_new = new_with_label Gtk::TreeItem( $dir_entry );
		$item_new->set_user_data( $path );
		$item_new->signal_connect( 'select', \&select_item, $path );
		$subtree->append( $item_new );
		$item_new->show();

		# Add plus sign
		#	I do this in all cases - it's faster that way.
		$new_subtree = new Gtk::Tree();
		$item_new->set_subtree( $new_subtree );
		$item_new->signal_connect( 'expand',
                                               \&expand_tree,
                                               $new_subtree );
		$item_new->signal_connect( 'collapse', \&collapse_tree );
	 }
}


sub collapse_tree
# Callback for collapsing a tree -- removes the subtree
{
	my ( $item ) = @_;

	my $subtree = new Gtk::Tree();

	$item->remove_subtree();
	$item->set_subtree( $subtree );
	$item->signal_connect( 'expand', \&expand_tree, $subtree );
}


sub select_item
# Called whenever an item is clicked on the tree widget.
{
	my ( $widget, $path ) = @_;

	$entry->set_text( $path );

	show_files( $path );
	$list_row = -1;
}


sub entry_activate
# Called when enter is pressed in the Entry widget
{
	my ( $entry ) = @_;

	my $path = $entry->get_text();

	show_files( "$path" );
	$list_row = -1;
}


sub show_files
# Fill the list with files from the provided directory
{
	my ( $path ) = @_;

	my $file;
	my $cached;
	my @file_entries;

	$list->clear();

	@file_entries = remote_files( "$PathPrefix/$path" );

	for $file ( @file_entries )
	{
		# Filename
		$file =~ s|/.*/||g;

		# Cache status
		$cached = "";
		if ( cache_query( "$PathPrefix/$path/$file" ) )
		{
			$cached = "Cached";
		}

		# Add
		$list->append( $file, $cached );
	}
}


sub select_callback
# When a list row is selected
{
	my ( $clist, $row, $column, $event, @data ) = @_;

	$list_row = $row;
}


sub unselect_callback
# when a list row is unselected
{
	$list_row = -1;
}


sub refresh_callback
# Reload the file list
# Called both from refresh button and from timer
{
	# Refresh display
	my $path = $entry->get_text();
	show_files( "$path" );
	$list->moveto( $list_row, 0, 0.50, 0.0);
	$list->select_row( $list_row, 0);
}


sub encache_button_callback
# Encache button callback
{
	my ( $widget, $clist ) = @_;
	
	# Variables
	my $filename;
	my $path;

	# Protections
	if (-1 == $list_row)
	{
		return;
	}

	# Set cursor
	window_cursor_watch();

	# Get path pieces
	$path = $entry->get_text();
	$filename = $clist->get_text( $list_row, 0 );

	# Cache it!
	cache_add("$PathPrefix/$path/$filename");
	
	# Set cursor
	window_cursor_arrow();
}


sub enqueue_button_callback
# Enqueque button callback
{
	my ( $widget, $clist ) = @_;

	# Variables
	my $filename;
	my $localfile;
	my $path;
	my $command;
	my $cursor;

	# Protections
	if (-1 == $list_row)
	{
		return;
	}

	# Set cursor
	window_cursor_watch();

	# Get path pieces
	$path = $entry->get_text();
	$filename = $clist->get_text( $list_row, 0 );

	# Cache it!
	cache_add("$PathPrefix/$path/$filename");

	# Run
	$localfile = cache_hash( "$Hostname/$PathPrefix/$path/$filename" );
	$localfile = "$Cachedir/$localfile";
	$localfile = shellify( $localfile );

	# Compose command
	$command = "xmms -m -e $localfile &";

	# Call command
	system($command);

	# Set cursor back
	window_cursor_arrow();
}


sub play_button_callback
{
	my ( $widget, $clist ) = @_;

	# Variables
	my $filename;
	my $localfile;
	my $path;
	my $command;

	# Protections
	if (-1 == $list_row)
	{
		return;
	}

	# Set cursor
	window_cursor_watch();

	# Get path pieces
	$path = $entry->get_text();
	$filename = $clist->get_text( $list_row, 0 );

	# Cache it!
	cache_add("$PathPrefix/$path/$filename");

	# Run
	$localfile = cache_hash( "$Hostname/$PathPrefix/$path/$filename" );
	$localfile = "$Cachedir/$localfile";
	$localfile = shellify( $localfile );

	# Compose command
	$command = "xmms -m -p $localfile &";

	# Call command
	system($command);
	
	# Set cursor
	window_cursor_arrow();
}


sub quit_button_callback
# Quit button callback
{
	exit(0);
}


