#!/usr/bin/perl -w # Xalbumlist 2.4 # Copyright (C) 2003 James Curbo # http://xalbumlist.sf.net # Licensed under the GPL. # # See ChangeLog for change details, and AUTHORS for others who helped # me with this program. ######################################################### # to add in changelog (from 2.3) # by Twidi # + upgarding preferences window # - adding "save columns position" # and "covers size" options # - many visual improvments # + better system for swapping with/without covers # + new config system # + thumbnails for covers # + many albums can now have the same name # + upgrading scanning window ######################################################### use strict; use warnings; use Gtk2::GladeXML; use Gtk2::SimpleList; use File::Find; use File::Basename; use IO::Handle; use Digest::MD5 qw(md5_hex); use Data::Dumper; use Xmms::Remote; use MP3::Info; use constant TRUE => 1; use constant FALSE => 0; my $remote = Xmms::Remote->new(); Gtk2->init; # our name my $softname = 'XAlbumList'; # our version number my $version = '2.4'; # our config file my $config_dir = $ENV{'HOME'}.'/.config/'.lc($softname); my $thumbnails_dir = $config_dir.'/thumbnails'; my $config_file = $config_dir . '/'.lc($softname).'.conf'; my $data_file = $config_dir . '/'.lc($softname).'.data'; my %config; # flag for computing times of mp3s $config{'find_time'} = TRUE; # flag for saving size and position $config{'save_size'} = FALSE; $config{'save_columns'} = FALSE; # flag for showing covers $config{'show_covers'} = TRUE; # covers size : 0->2, 2 = bigest $config{'covers_size'} = 0; read_config(); # list of directories my @dirlist; # count of directories, used for the scanning window my %count = (dir => 0, total => 0, albums => 0); # list of albums (entries) my %albumlist; load_data(); # for fast access my %md5_cache; # glade init my $xml; { local $/ = undef; $xml = ; } # for testing #my $gld = Gtk2::GladeXML->new(lc($softname).'.glade'); my $gld = Gtk2::GladeXML->new_from_buffer($xml); $gld->signal_autoconnect_from_package('main'); # get main window and other widgets my $main_window = $gld->get_widget('window_main'); my (%pref_check, %pref_list); map { $pref_check{$_} = $gld->get_widget('check_button_'.$_); } qw (find_time save_size save_columns show_covers); map { $pref_list{$_} = $gld->get_widget('option_menu_'.$_); } qw (covers_size); my $progressbar = $gld->get_widget('progressbar_scan'); # default images if no covers my @pixbuf_sizes = qw(button large-toolbar dialog); my @pixbuf_px = qw(16 24 48); my @pixbuf_default; map { $pixbuf_default[$_] = $main_window->render_icon('gtk-open', $pixbuf_sizes[$_]); } (0..$#pixbuf_sizes); # init dirlist my $dirlist_widget = Gtk2::SimpleList->new('Directory' => 'text'); my $sw_dir = $gld->get_widget('scrolledwindow_dirlist'); $sw_dir->add($dirlist_widget); # init mainlist my @columns; my $sw_main = $gld->get_widget('scrolledwindow_main'); my $mainlist = create_mainlist(); show_mainlist(); # initialize globals and stuff update_window(); update_preferences(); update_mainlist(); update_dirlist(); if (! -e $config_file) { mkdir($config_dir) if ! -d $config_dir; on_preferences_activate(); } mkdir($thumbnails_dir) if ! -d $thumbnails_dir; # go into main loop Gtk2->main; # do this before quitting write_config(); save_data(); # done 1; ############################################################ # functions below here sub read_config { my @data; if (-e $config_file) { open CONFIG, "< $config_file"; while () { chomp; if ($_ =~ /^config_(.+?) = (.*)/) { $config{$1} = $2; } elsif ($_ =~ /^dir = (.*)/) { push @dirlist, $1; } } close CONFIG; } $config{'find_time'} = TRUE if !exists($config{'find_time'}) or $config{'find_time'} !~ /^[0|1]$/; $config{'save_size'} = FALSE if !exists($config{'save_size'}) or $config{'save_size'} !~ /^[0|1]$/ or !exists($config{'win_left'}) or $config{'win_left'} !~ /^\d+$/ or !exists($config{'win_top'}) or $config{'win_top'} !~ /^\d+$/ or !exists($config{'win_width'}) or $config{'win_width'} !~ /^\d+$/ or !exists($config{'win_height'}) or $config{'win_height'} !~ /^\d+$/; $config{'save_columns'} = TRUE if !exists($config{'save_columns'}) or $config{'save_columns'} !~ /^[0|1]$/; map { delete($config{'column_'.$_.'_size'}) if exists($config{'column_'.$_.'_size'}) and $config{'column_'.$_.'_size'} !~ /^\d+$/} @columns if $config{'save_columns'}; $config{'show_covers'} = TRUE if !exists($config{'show_covers'}) or $config{'show_covers'} !~ /^[0|1]$/; $config{'covers_size'} = 2 if !exists($config{'covers_size'}) or $config{'show_covers'} !~ /^[0|1|2]$/; } sub write_config { open CONFIG, "> $config_file" or die "Couldn't open config file for writing: $!"; map { print CONFIG 'config_'.$_.' = '.$config{$_}."\n"; } qw (find_time save_size save_columns show_covers covers_size); map { print CONFIG 'config_win_'.$_.' = '.$config{'win_'.$_}."\n"; } qw (left top width height) if $config{'save_size'}; map { print CONFIG 'config_column_'.$_.'_size = '.$config{'column_'.$_.'_size'}."\n"; } @columns if $config{'save_columns'}; for (sort @dirlist) { print CONFIG "dir = $_\n"; } close CONFIG; } sub load_data { open DATAFILE, "< $data_file"; my @dump = ; close DATAFILE; @dump = ('$VAR1 = {}') if !@dump; my $VAR1; %albumlist = %{eval("@dump")}; } sub save_data { open DATA, "> $data_file" or die "Couldn't open data file for writing: $!"; print DATA Dumper(\%albumlist); close DATA; } sub update_window { # set size and position if needed if ($config{'save_size'}) { # the main window $main_window->move($config{'win_left'}, $config{'win_top'}); $main_window->resize($config{'win_width'}, $config{'win_height'}); } # save columns position if ($config{'save_columns'}) { foreach my $column ($mainlist->get_columns()) { $column->set_sizing('fixed'); $column->set_fixed_width($config{'column_'.$column->get_title().'_size'}) if exists($config{'column_'.$column->get_title().'_size'}); } } # update version number in about dialog (default = 2.0) my $name_and_version = $gld->get_widget('label_top'); $name_and_version->set_markup(''.$softname.' '.$version.''); } sub get_md5 { my $value = shift; $md5_cache{$value} = md5_hex($value) if !exists($md5_cache{$value}); return $md5_cache{$value}; } sub find_dirs { # $File::Find::dont_use_nlink = 1; Gtk2->main_iteration while Gtk2->events_pending; if (-d $File::Find::name) { $progressbar->set_fraction($count{'dir'} / $count{'total'}); my $album_id = get_md5($File::Find::name); opendir(DIR, $File::Find::name); $count{'dir'}++; my $dir_ok = FALSE; foreach my $file (sort(readdir(DIR))) { next if $file =~ /^\.\.?$/; next if -d $file; if ($config{'show_covers'}) { if (!exists($albumlist{$album_id}->{COVER}) and $file =~ /\.(png|jpg|gif|bmp)$/i) { $albumlist{$album_id}->{COVER} = $File::Find::name.'/'.$file; last if $dir_ok; } } else { last if $dir_ok; } if (!$dir_ok and $file =~ /\.(mp3|ogg|mpc|ape|flac|shn|s3m|it|mod|mid)$/i) { $count{'albums'}++; $albumlist{$album_id}->{PATH} = $File::Find::name; $albumlist{$album_id}->{KEY} = get_md5($File::Find::name); $albumlist{$album_id}->{ALBUM} = basename($File::Find::name); $albumlist{$album_id}->{NUMSONGS} = '?'; $albumlist{$album_id}->{TIME} = "?:??:??"; update_album_info($album_id); my $label1 = $gld->get_widget('label_scan'); my $label2 = $gld->get_widget('label_number'); $label1->set_text($albumlist{$album_id}->{ALBUM}); $label2->set_text("Directories scanned: $count{'dir'} / $count{'total'}, $count{'albums'} albums found"); $dir_ok = TRUE; last if !$config{'show_covers'} or exists($albumlist{$album_id}->{COVER}); } } delete($albumlist{$album_id}) if exists($albumlist{$album_id}) and !$dir_ok; # if only the cover, delete it if ($config{'show_covers'} and exists($albumlist{$album_id}) and exists($albumlist{$album_id}->{COVER}) and -e $albumlist{$album_id}->{COVER}) { my $thumbname = $thumbnails_dir.'/'.$albumlist{$album_id}->{KEY}; foreach my $size (0..$#pixbuf_sizes) { if (!-e $thumbname.'_cover_'.$size.'.jpg') { my $px = $pixbuf_px[$size]; my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file_at_size($albumlist{$album_id}->{COVER}, $px, $px); $pixbuf->save($thumbname.'_cover_'.$size.'.jpg', 'jpeg', quality => '80'); } } } } } sub count_dirs { Gtk2->main_iteration while Gtk2->events_pending; if (-d $File::Find::name) { $count{'total'}++; my $label1 = $gld->get_widget('label_scan'); my $label2 = $gld->get_widget('label_number'); $label1->set_text($File::Find::name); $label2->set_text("Directories found: $count{'total'}"); } } sub create_mainlist { my $list = Gtk2::SimpleList->new( 'Key' => 'hidden', '' => $config{'show_covers'} ? 'pixbuf' : 'hidden', 'Album' => 'text', 'Songs' => 'text', 'Time' => 'text', 'Path' => 'text',); read_columns_infos($list); $list->get_selection->set_mode('multiple'); $list->set_rules_hint(TRUE); $list->set_headers_clickable(TRUE); foreach ($list->get_columns()) { $_->set_resizable(TRUE); $_->set_sizing("grow-only"); } $list->signal_connect('row-activated' => \&on_mainlist_row_activated); return $list; } sub show_mainlist { $sw_main->add($mainlist); $mainlist->show; } sub update_albumlist { $progressbar->set_fraction(0); my $win = $gld->get_widget('window_scanning'); $win->show_all; $count{'dir'} = $count{'total'} = $count{'albums'} = 0; unless ($#dirlist == -1) { find(\&count_dirs, @dirlist); finddepth(\&find_dirs, @dirlist); } $win->hide; } sub update_mainlist { @{$mainlist->{data}} = (); foreach my $album_id (sort { $albumlist{$a}->{'ALBUM'} cmp $albumlist{$b}->{'ALBUM'} } keys %albumlist) { # cover my $file = $thumbnails_dir.'/'.$albumlist{$album_id}->{'KEY'}.'_cover_'.$config{'covers_size'}.'.jpg'; my $pixbuf = ($config{'show_covers'} and -e $file) ? Gtk2::Gdk::Pixbuf->new_from_file($file) : $pixbuf_default[$config{'covers_size'}]; # adding album push @{$mainlist->{data}}, [ $albumlist{$album_id}->{'KEY'}, $config{'show_covers'} ? $pixbuf : '', $albumlist{$album_id}->{'ALBUM'}, $albumlist{$album_id}->{'NUMSONGS'}, $albumlist{$album_id}->{'TIME'}, $albumlist{$album_id}->{'PATH'} ]; } $mainlist->select(0); } sub update_dirlist { @{$dirlist_widget->{data}} = @dirlist; } sub read_columns_infos { my $list = shift; @columns = (); map { push @columns, $_->get_title(); } $list->get_columns(); } sub gtk_main_quit { # saving size and position if ($config{'save_size'}) { # the main window ($config{'win_left'}, $config{'win_top'}) = $main_window->get_position(); ($config{'win_width'}, $config{'win_height'}) = $main_window->get_size(); } # saving columns position if ($config{'save_columns'}) { map { $config{'column_'.$_->get_title().'_size'} = $_->get_width(); } $mainlist->get_columns(); } Gtk2->main_quit; } # only used for dialogs, the main window calls gtk_main_quit # doesn't REALLY destroy the window, as glade won't recreate it # so we just hide it sub on_wm_close_clicked { my $window = shift; $window->hide; return TRUE; } # buttons on the main toolbar sub on_button_add_clicked { my $button = shift; my $dialog = $gld->get_widget('dialog_dirlist') or die "Couldn't create dialog: $!"; $dialog->show_all; } sub on_button_refresh_clicked { update_albumlist(); update_mainlist(); } sub on_button_clear_clicked { %albumlist = (); update_mainlist(); } sub on_button_load_clicked { my @albums = $mainlist->get_selected_indices(); my @done; # if nothing is selected, just act like nothing happened unless ( $#albums == -1 ) { $remote->playlist_clear(); foreach (@albums) { push @done, $albumlist{$mainlist->{data}[$_][0]}->{'PATH'}; } $remote->playlist_add(\@done); $remote->play; } } sub on_button_enqueue_clicked { my @albums = $mainlist->get_selected_indices(); my @done; unless ($#albums == -1) { foreach (@albums) { push @done, $albumlist{$mainlist->{data}[$_][0]}->{'PATH'}; } $remote->playlist_add(\@done); } } # buttons in the Add Directories dialog sub on_okbutton_dirlist_clicked { my $widget = shift; $widget->get_toplevel->hide; update_albumlist(); update_mainlist(); } sub on_cancelbutton_dirlist_clicked { my $widget = shift; $widget->get_toplevel->hide; } sub on_button_diradd_clicked { my $button = shift; my $filesel = Gtk2::FileSelection->new("Change to the directory you wish to add, then press OK"); $filesel->ok_button->signal_connect("clicked", \&on_filesel_okbutton_clicked, $filesel); $filesel->cancel_button->signal_connect("clicked", sub { $filesel->destroy;}); $filesel->show_all; } sub on_filesel_okbutton_clicked { my ($widget, $filesel) = @_; my $file = $filesel->get_filename; if (-d $file) { # add to the treeview add_to_dirlist($file); } $filesel->destroy; } sub add_to_dirlist { my $file = shift; # check to make sure this directory is not already in the list my $is_present = 0; foreach my $item (@dirlist) { if ($item eq $file) { $is_present = 1; } } # if it isn't, add it unless ($is_present) { push @dirlist, $file; push @{$dirlist_widget->{data}}, [$file]; } } sub on_button_dirdel_clicked { my @selected = $dirlist_widget->get_selected_indices(); unless ($#dirlist == -1) { foreach (@selected) { splice(@dirlist, $_, 1); } } update_dirlist(); } # menu stuff sub on_about_activate { my $dialog = $gld->get_widget('dialog_about'); $dialog->show_all; } sub on_okbutton_about_clicked { my $widget = shift; $widget->get_toplevel->hide; } sub update_album_info { my ($album_id) = @_; my $num_songs = 0; my @total_time = (0,0,0); my $time; my $info; if ($config{'find_time'}) { opendir(DIR, $albumlist{$album_id}->{'PATH'}) or die "couldn't open dir: $!"; while (my $file = readdir(DIR)) { next unless $file =~ /\.(mp3|ogg|mpc|ape|flac|shn|s3m|it|mod|mid)$/i; $num_songs++; next unless $file =~ /\.mp3$/i; $info = get_mp3info($albumlist{$album_id}->{'PATH'} . "/" . $file); $total_time[1] += $info->{MM} if exists $info->{MM}; $total_time[2] += $info->{SS} if exists $info->{SS}; } closedir(DIR); # convert time $total_time[1] += ($total_time[2] - ($total_time[2] % 60))/ 60; $total_time[2] = $total_time[2] % 60; $total_time[0] = ($total_time[1]-($total_time[1] % 60))/ 60; $total_time[1] = $total_time[1] % 60; $time = sprintf('%01d:%02d:%02d', @total_time); $albumlist{$album_id}->{'NUMSONGS'} = $num_songs; $albumlist{$album_id}->{'TIME'} = $time; } } sub on_mainlist_row_activated { on_button_load_clicked(); } # preference window sub update_preferences { $pref_check{'find_time'}->set_property('active', $config{'find_time'}); $pref_check{'save_size'}->set_property('active', $config{'save_size'}); $pref_check{'save_columns'}->set_property('active', $config{'save_columns'}); $pref_check{'show_covers'}->set_property('active', $config{'show_covers'}); $pref_list{'covers_size'}->set_history(2-$config{'covers_size'}); } sub on_preferences_activate { my $dialog = $gld->get_widget('dialog_preferences'); $dialog->show_all; } sub on_okbutton_preferences_clicked { # hide the preference dialog my $widget = shift; $widget->get_toplevel->hide; # apply preferences on_applybutton_preferences_clicked(); } sub on_applybutton_preferences_clicked { my %old_config = (%config); # save preferences in config hash $config{'find_time'} = $pref_check{'find_time'}->get_active() ? TRUE : FALSE; $config{'save_size'} = $pref_check{'save_size'}->get_active() ? TRUE : FALSE; $config{'save_columns'} = $pref_check{'save_columns'}->get_active() ? TRUE : FALSE; $config{'show_covers'} = $pref_check{'show_covers'}->get_active() ? TRUE : FALSE; $config{'covers_size'} = 2-$pref_list{'covers_size'}->get_history(); my $redraw_mainlist = (($config{'show_covers'} != $old_config{'show_covers'}) or ($config{'covers_size'} != $old_config{'covers_size'})); if ($redraw_mainlist) { my $old_mainlist = $mainlist; $mainlist = create_mainlist(); update_mainlist(); $old_mainlist->destroy(); show_mainlist(); } } sub on_show_covers_clicked { my $widget = shift; $pref_list{'covers_size'}->set_sensitive($widget->get_active() ? 1 : undef); } sub on_cancelbutton_preferences_clicked { # hide the preference dialog my $widget = shift; $widget->get_toplevel->hide; # restore old preferences update_preferences(); } __DATA__ True XAlbumList GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE False 600 400 True False True False 0 True True _File True True gtk-preferences True True True gtk-quit True True True _Help True True gtk-about True True 0 False False True GTK_ORIENTATION_HORIZONTAL GTK_TOOLBAR_BOTH True True True Add gtk-add True True True True Refresh gtk-refresh True True True True Clear gtk-clear True True True True True True True False False True Load gtk-apply True True True True Enqueue gtk-goto-last True True True True True True True False False True Preferences gtk-preferences True True True 0 False False True True GTK_POLICY_AUTOMATIC GTK_POLICY_AUTOMATIC GTK_SHADOW_NONE GTK_CORNER_TOP_LEFT 0 True True Add Directories GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE False 300 200 True False True True False 0 True GTK_BUTTONBOX_DEFAULT_STYLE True True True gtk-cancel True GTK_RELIEF_NORMAL -5 True True True gtk-ok True GTK_RELIEF_NORMAL -5 0 False True GTK_PACK_END True True GTK_POLICY_AUTOMATIC GTK_POLICY_AUTOMATIC GTK_SHADOW_NONE GTK_CORNER_TOP_LEFT 0 True True True False 0 50 32 True True gtk-add True GTK_RELIEF_NORMAL 30 True True 50 32 True True GTK_RELIEF_NORMAL True 0.5 0.5 0 0 True False 2 True gtk-remove 4 0.5 0.5 0 0 0 False False True Delete True False GTK_JUSTIFY_LEFT False False 0.5 0.5 0 0 0 False False 30 True True 10 False False Preferences GTK_WINDOW_TOPLEVEL GTK_WIN_POS_MOUSE True True True True True False 4 True GTK_BUTTONBOX_DEFAULT_STYLE True True True gtk-cancel True GTK_RELIEF_NORMAL -6 True True True gtk-apply True GTK_RELIEF_NORMAL -10 True True True gtk-ok True GTK_RELIEF_NORMAL -5 0 False True GTK_PACK_END True False 2 True 0 0.5 GTK_SHADOW_ETCHED_IN 8 True False 4 True <i>For each songs in each folders, get time in order to calcule album's duration. Also get songs' number.</i> 0 0.5 False True 0 True True True <i>If checked, scanning may be (very) long function of number of directories.</i> 0 0.5 False True 0 True True True Calculate time? (You should click the refresh button after checking and validating) 0 False False 0 True True True <b>Calculate albums</b> True label_item 3 True True True 0 0.5 GTK_SHADOW_ETCHED_IN 8 True False 4 True Save size and position of main window 0 False False True Save position of all columns 0 False False True <b>Sizes and positions</b> True label_item 3 False True True 0 0.5 GTK_SHADOW_ETCHED_IN 8 True False 4 True <i>The first image in a directory will be the cover for the associated album.</i> 0 0.5 False True 0 False False True <i>If checked, scanning may be long the first time covers are loaded.</i> 0 0.5 False True 0 False False True Show covers on main list 0 False False True False 0 True Covers size : 0 False False True True 0 True Large True True Medium True True Small True 0 False False 0 False False True <b>Covers</b> True label_item 3 False True 0 True True About xalbumlist GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE False 300 140 True False True True False 0 True GTK_BUTTONBOX_DEFAULT_STYLE True True True gtk-ok True GTK_RELIEF_NORMAL -5 0 False True GTK_PACK_END True False 0 True <b><big>xalbumlist 2.0</big></b> False True GTK_JUSTIFY_CENTER False False 0.5 0.5 0 0 0 False False True Control Program for XMMS Copyright (C) 2003 James Curbo <hannibal@adtrw.org> http://xalbumlist.sf.net/ Licensed under the GPL. False False GTK_JUSTIFY_CENTER True False 0.5 0.5 0 0 0 True True 0 True True 500 0 Scanning... GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE True False True True False 0 True False False GTK_JUSTIFY_LEFT False False 0.5 0.5 0 0 4 False False True False False GTK_JUSTIFY_LEFT False False 0.5 0.5 0 0 0 False False True GTK_PROGRESS_LEFT_TO_RIGHT 0 0.1 0 False False