├── AUTHORS ├── LICENSE ├── README.md ├── screenshot.png └── whistle /AUTHORS: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | 3 | Manuel Fill - ap0calypse (manuel.fill.42@gmail.com) 4 | 5 | CONTRIBUTORS 6 | 7 | Matthew Cox 8 | smiszym 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Fill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About: 2 | ------ 3 | 4 | whistle aims to be a very minimalistic and basic audio player. 5 | Currently it can play mp3, flac and ogg files. 6 | 7 | Basically, whistle is a nifty front-end to mplayer, which it's 8 | based on. Without mplayer, there would be no whistle. The earlier 9 | mpg123 back-end work is now completely done by mplayer. 10 | 11 | 12 | Latest screenshot: 13 | ------------------ 14 | 15 | ![screenshot whistle](screenshot.png "Screenshot") 16 | 17 | 18 | Prerequisites: 19 | -------------- 20 | 21 | Perl Modules: 22 | 23 | - MP3::Info (archlinux-package: perl-mp3-info) 24 | - Curses::UI (archlinux-package: perl-curses-ui) 25 | - File::MimeInfo (archlinux-package: perl-file-mimeinfo) 26 | - Ogg::Vorbis::Header::PurePerl (aur-package: perl-ogg-vorbis-header-pureperl) 27 | - Audio::FLAC::Header (aur-package: perl-audio-flac-header) 28 | - LWP::UserAgent 29 | 30 | Programs: 31 | 32 | - mplayer (mpg123 earlier) 33 | 34 | 35 | Installation: 36 | ------------- 37 | 38 | ###ArchLinux: 39 | 40 | packer -S mplayer perl-ogg-vorbis-header-pureperl \ 41 | perl-audio-flac-header perl-mp3-info perl-curses-ui \ 42 | perl-file-mimeinfo whistle-git 43 | 44 | (you maybe need to replace 'packer' with your custom AUR-helper (yaourt, clyde, ...) 45 | 46 | 47 | ###Debian: 48 | 49 | 1. apt-get install git mplayer libncurses-ui-perl \ 50 | libmp3-info-perl libogg-vorbis-header-pureperl-perl \ 51 | ibaudio-flac-header-perl libfile-mimeinfo-perl 52 | 2. clone the git repository: (git clone https://github.com/ap0calypse/whistle.git) 53 | 3. run whistle and enjoy :) 54 | 55 | ###Slackware: 56 | 57 | Whistle can be installed following these steps (all listed packages are either needed by whistle or are dependencies of each other) : 58 | 59 | 1. install sbopkg (http://www.sbopkg.org/downloads.php,-> 60 | # installpkg sbopkg-version-noarch-1_cng.tgz) 61 | 2. run sbopkg and sync with slackbuilds.org (Sync with remote repository) 62 | 3. install the following packages: 63 | - perl-Audio-FLAC-Header 64 | - perl-Curses 65 | - perl-Curses-UI 66 | - perl-File-Which 67 | - perl-IPC-Run3 68 | - perl-IPC-System-Simple 69 | - perl-MP3-Info 70 | - perl-Module-Build 71 | - perl-Ogg-Vorbis-Header-PurePerl 72 | - perl-Probe-Perl 73 | - perl-TermReadKey 74 | - perl-Test-Script 75 | - perl-ExtUtils-depends 76 | - perl-ExtUtils-makemaker 77 | - perl-ExtUtils-pkgconfig 78 | - perl-file-basedir 79 | - perl-file-desktopentry 80 | - perl-file-mimeinfo 81 | - whistle 82 | 4. run whistle and enjoy :) 83 | 84 | 85 | What works? a.k.a. Features: 86 | ---------------------------- 87 | 88 | - full MP3/OGG/FLAC play/stop/next/prev/seek/shuffle support 89 | - progressbar 90 | - playlists 91 | - multi-select for playlist-editing 92 | - better and finer granulated equalizer (10 band) 93 | - per-song/album-equalizer (meaning: individual equalizer settings 94 | for each title/album/artist) 95 | - lyrics fetching support 96 | - queue support (+/-) 97 | 98 | 99 | What's to come? 100 | --------------- 101 | 102 | - a lot of bugfixes for sure 103 | - shuffle by artist/album 104 | - equalizer presets 105 | - color support 106 | - mp3 tag write support 107 | - burn playlist 2 iso or cd 108 | - ... 109 | - your feature request? 110 | 111 | 112 | Usage: 113 | ------ 114 | 115 | whistle must be started within a fully functional terminal. 116 | 117 | The first step is to add a music directory with 'A'. The standard 118 | usage scenario then is to select what you want to play and press 'P'. 119 | 120 | 121 | Thanks: 122 | ------- 123 | - my girlfriend for beta-testing and giving me hints for improvements :) 124 | - all the people testing and helping me to improve whistle :) 125 | (kmandla, matthew cox, smiszym, ...) 126 | - all the supportive people of the arch-community :) 127 | - all developers behind mpg123, mplayer and mpv 128 | - all developers from Curses and Curses::UI 129 | - all developers from MP3::Info 130 | - all developers from Ogg::Vorbis::Header 131 | - all developers from Audio::FLAC::Header 132 | - mniip for debian install infos 133 | 134 | 135 | 136 | If you want to give me some crypto-credit, please use one of these adresses: 137 | 138 | - LTC - LcWms1wddhRKWyE7JDSxZcY6gZTGTAttXG 139 | - BTC - bc1qkggktuhhdms2ue6c04archpvc6h4pqfeu25y3h 140 | - DOGE - DRpqNpcfAidAFmkLzaLoB6gVrRgKzNtiRC 141 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ap0calypse/whistle/714f03da190c0a21075d8cf5efcd2738191e5d6c/screenshot.png -------------------------------------------------------------------------------- /whistle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use Curses::UI; 5 | use MP3::Info; 6 | use Ogg::Vorbis::Header::PurePerl; 7 | use Audio::FLAC::Header; 8 | use Storable qw(nstore retrieve); 9 | use File::Find; 10 | use IPC::Open2; 11 | use Curses qw(KEY_LEFT KEY_RIGHT KEY_F getmaxyx initscr endwin); 12 | use File::MimeInfo qw(mimetype describe); 13 | use Digest::MD5 qw(md5_hex); 14 | use LWP::UserAgent; 15 | 16 | # project: whistle 17 | # author: see AUTHORS 18 | # license: see LICENSE 19 | # purpose: see README 20 | 21 | # data location, hashref declaratiion, global vars 22 | our $STORFILE = $ENV{'HOME'} . "/.whistle.dat"; 23 | our $STORAGE; # storage hashref 24 | our $SHUFFLE = 0; # 0 = normal, 1 = random 25 | our $CUR_LIST = 'ALL'; # current playlist 26 | our $LAST_PLAYED = 0; 27 | 28 | # vars for mplayer's 10band equalizer (allowed: -12 <-> 12) 29 | our @EQ = (0.0 ,0.0 ,0.0 ,0.0 ,0.0 ,0.0 ,0.0 ,0.0 ,0.0 ,0.0); 30 | 31 | my ($dir_rem, $pl_add, $sel_pl); 32 | my $num_songs = 0; 33 | our $MAX_PL; 34 | our $STATE = 0; 35 | our @QUEUE = (); 36 | our %Q2INDEX; 37 | 38 | # turn on autoflush 39 | $| = 1; 40 | 41 | # don't hard code mplayer location 42 | open( my $WHICH, "which mplayer 2>/dev/null |" ); 43 | chomp( my( $mplayer ) = <$WHICH> ); 44 | close( $WHICH ); 45 | 46 | # open read and write handles for mpv 47 | my $MPLPID = open2(my $Mout, my $Min, "$mplayer -cache-min 10 -idle -slave &> /dev/null") or die "mplayer hates me :("; 48 | 49 | 50 | # read in data if available, otherwise initialise STORAGE hashref 51 | if (-e $STORFILE && (stat $STORFILE)[7] > 5) { 52 | $STORAGE = retrieve $STORFILE; 53 | } 54 | else { 55 | $STORAGE = { 'DIRS' => [], 'PLAYLIST' => {}}; 56 | $STORAGE->{'PLAYLIST'}{'ALL'} = {}; 57 | } 58 | 59 | # check for screen size 60 | initscr(); 61 | my ($row, $col); 62 | getmaxyx($row, $col); 63 | endwin(); 64 | 65 | print "your terminal windows' height is too low! (20 rows needed, you have $row)\n" and exit(0) if $row < 20; 66 | print "your terminal windows' length is too low! (80 cols needed, you have $col)\n" and exit(0) if $col < 80; 67 | 68 | # create the main ui and main window 69 | my $cui = new Curses::UI ( -clear_on_exit => 0, -color_support => 1); 70 | my $win = $cui->add('win', 'Window', -border => 1, -title => "whistle"); 71 | 72 | 73 | # create current playlist 'window' 74 | my $curlist = $win->add( 75 | 'curlist', 'Listbox', 76 | -fg => 'white', 77 | -multi => 1, 78 | -title => "Playlist: $CUR_LIST", 79 | -readonly => 1, 80 | -y => 0, 81 | -x => 52, 82 | -focusable => 1, 83 | -nocursor => 0, 84 | -vscrollbar => 'right', 85 | -wrapping => 1, 86 | -intellidraw => 1, 87 | -border => 1, 88 | -ipad => 1, 89 | -htmltext => 1, 90 | ) or die "curlist"; 91 | 92 | # create controls 'window' 93 | my $equalizer = $win->add( 94 | 'controls', 'TextViewer', 95 | -fg => 'white', 96 | -title => "Equalizer (F1-F10)", 97 | -readonly => 1, 98 | -y => 3, 99 | -x => 25, 100 | -focusable => 0, 101 | -wrapping => 0, 102 | -height => 6, 103 | -width => 27, 104 | -intellidraw => 1, 105 | -border => 1, 106 | ) or die "controls"; 107 | 108 | # lyrics window, 109 | my $lyrics = $win->add( 110 | 'lyrics', 'TextViewer', 111 | -fg => 'white', 112 | -title => "Lyrics (L)", 113 | -readonly => 1, 114 | -y => 16, 115 | -x => 0, 116 | -focusable => 1, 117 | -wrapping => 1, 118 | -vscrollbar => 'right', 119 | -width => 52, # thats the width, period, nothing more, nothing less 120 | -height => 16, 121 | -intellidraw => 1, 122 | -border => 1, 123 | ) or die "lyrics"; 124 | 125 | my $queue = $win->add( 126 | 'queue', 'TextViewer', 127 | -fg => 'white', 128 | -title => "Queue (+/-)", 129 | -readonly => 1, 130 | -y => 32, 131 | -x => 0, 132 | -focusable => 0, 133 | -wrapping => 0, 134 | -vscrollbar => 'right', 135 | -width => 52, # thats the width, period, nothing more, nothing less 136 | -intellidraw => 1, 137 | -border => 1, 138 | ) or die "queue"; 139 | 140 | # create playlist 'window' 141 | my $playlists = $win->add( 142 | 'playlists', 'TextViewer', 143 | -fg => 'white', 144 | -title => "Playlists", 145 | -readonly => 1, 146 | -y => 0, 147 | -x => 0, 148 | -focusable => 0, 149 | -wrapping => 0, 150 | -height => 16, 151 | -width => 25, 152 | -intellidraw => 1, 153 | -border => 1, 154 | -ipad => 0, 155 | ) or die "playlists"; 156 | 157 | # create directory 'window' 158 | my $directories = $win->add( 159 | 'directories', 'TextViewer', 160 | -fg => 'white', 161 | -title => "Directories", 162 | -readonly => 1, 163 | -y => 9, 164 | -x => 25, 165 | -focusable => 0, 166 | -wrapping => 1, 167 | -height => 7, 168 | -width => 27, 169 | -intellidraw => 1, 170 | -border => 1, 171 | -ipad => 0, 172 | ) or die "directories"; 173 | 174 | my $progressbar = $win->add( 175 | 'progress', 'Progressbar', 176 | -max => 100, 177 | -pos => 0, 178 | -y => 0, 179 | -x => 25, 180 | -focusable => 0, 181 | -wrapping => 1, 182 | -height => 3, 183 | -width => 27, 184 | -intellidraw => 1, 185 | -title => "--:-- / --:--", 186 | -border => 1, 187 | ) or die "progressbar"; 188 | 189 | # pretty nasty shit, but neccessary for continuous playing 190 | our $TIMER = {'rest_time' => 0, 'max_time' => 0}; 191 | $SIG{ALRM} = sub { 192 | $TIMER->{'rest_time'}--; 193 | my $one_perc = $TIMER->{'max_time'} / 100; 194 | my $new_pro = int(($TIMER->{'max_time'} - $TIMER->{'rest_time'}) / $one_perc); 195 | my $diff = int($TIMER->{'max_time'} - $TIMER->{'rest_time'}); 196 | my $M_rest = int($diff / 60); 197 | my $S_rest = int($diff % 60); 198 | my $M_max = int($TIMER->{'max_time'} / 60); 199 | my $S_max = int($TIMER->{'max_time'} % 60); 200 | my $time_line = sprintf("%02d:%02d / %02d:%02d", $M_rest, $S_rest, $M_max, $S_max); 201 | $progressbar->title($time_line); 202 | $progressbar->pos($new_pro); 203 | $progressbar->draw(); 204 | if ($TIMER->{'rest_time'} <= 0) { 205 | &play('NEXT'); 206 | } 207 | else { 208 | alarm(1); 209 | } 210 | }; 211 | 212 | # draw the menu 213 | sub draw_menu { 214 | $curlist->clear_selection(); 215 | my $dirtxt; 216 | if (scalar @{$STORAGE->{'DIRS'}}) { 217 | for (@{$STORAGE->{'DIRS'}}) { 218 | $dirtxt .= "$_\n"; 219 | } 220 | } 221 | else { 222 | $dirtxt = "no directories given"; 223 | } 224 | $directories->text($dirtxt); 225 | $directories->draw(); 226 | my $pltxt; 227 | $playlists->text(''); 228 | $playlists->draw(); 229 | $pltxt .= $CUR_LIST eq 'ALL' ? "[*] " : "[ ] "; 230 | $pltxt .= "ALL"; 231 | $pltxt .= sprintf(" (%d)\n", scalar keys %{$STORAGE->{'PLAYLIST'}{'ALL'}}); 232 | for (grep {!/ALL/} sort keys %{$STORAGE->{'PLAYLIST'}}) { 233 | $pltxt .= $CUR_LIST eq $_ ? "[*] " : "[ ] "; 234 | $pltxt .= "\'-> $_"; 235 | $pltxt .= sprintf(" (%d)\n", scalar keys %{$STORAGE->{'PLAYLIST'}{$_}}); 236 | } 237 | $playlists->text($pltxt); 238 | $playlists->draw(); 239 | my %pl; 240 | $curlist->values(''); 241 | my $pl_w = int($curlist->width() / 9); 242 | my ($pl_t, $pl_a, $pl_al, $pl_d) = (int($pl_w * 3) , int($pl_w * 1), int($pl_w * 2), int ($pl_w)); 243 | $pl_t = "$pl_t.$pl_t" . "s"; 244 | $pl_a = "$pl_a.$pl_a" . "s"; 245 | $pl_al = "$pl_al.$pl_al" . "s"; 246 | $pl_d = "$pl_d.$pl_d" . "s"; 247 | my $j = 0; 248 | if (%{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}) { 249 | 250 | 251 | for (sort {$STORAGE->{'PLAYLIST'}{$CUR_LIST}{$a}{'artist'} cmp $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$b}{'artist'}} 252 | sort {$STORAGE->{'PLAYLIST'}{$CUR_LIST}{$a}{'album'} cmp $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$b}{'album'}} keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}) { 253 | my $full_line = sprintf(" %$pl_t | %$pl_a | %$pl_al | %$pl_d", $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'title'}, 254 | $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'artist'}, 255 | $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'album'}, 256 | $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'duration'}); 257 | $pl{$j} = $full_line; 258 | $curlist->insert_at($j, $pl{$j}); 259 | $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} = $j; 260 | $j++; 261 | } 262 | } 263 | else { 264 | $curlist->values("nothing found"); 265 | } 266 | $curlist->values(sort { $a <=> $b } keys %pl); 267 | $curlist->labels(\%pl); 268 | $curlist->draw(); 269 | $equalizer->text(sprintf("%4d %4d %4d %4d %4d\n", 1 .. 5) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[0..4]) . 270 | sprintf("%4d %4d %4d %4d %4d\n", 6 .. 10) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[5..9])); 271 | $equalizer->draw(); 272 | $progressbar->draw(); 273 | $lyrics->draw(); 274 | } 275 | 276 | sub draw_queue { 277 | my $qtxt = ""; 278 | my $qnum = 0; 279 | for (@QUEUE) { 280 | $qtxt .= "[$qnum] " . $STORAGE->{'PLAYLIST'}{'ALL'}{$_}{'artist'} . " - " . $STORAGE->{'PLAYLIST'}{'ALL'}{$_}{'title'} . "\n"; 281 | $qnum++; 282 | } 283 | $queue->text($qtxt); 284 | $queue->draw(); 285 | } 286 | 287 | # store the storage hashref to .whistle.dat 288 | sub store_data { 289 | nstore($STORAGE, $STORFILE); 290 | } 291 | 292 | 293 | # quit the program, close filehandles, store data 294 | sub quit_program { 295 | store_data(); 296 | print $Min "quit\n"; 297 | close $Min; 298 | close $Mout; 299 | kill $MPLPID; 300 | exit(0); 301 | } 302 | 303 | # find-function for found mp3's. writes returned data to storage hashref 304 | sub found_mp3 { 305 | my $mime = mimetype($File::Find::name); 306 | my $mime_long = describe($mime); 307 | if ($mime_long =~ m/^MP3 audio$/) { 308 | my $md5sum = md5_hex($File::Find::name); 309 | my $taghash = get_mp3tag($File::Find::name); 310 | my $infohash = get_mp3info($File::Find::name); 311 | my $esc_name = $File::Find::name; 312 | $esc_name =~ s/ /\\ /g; 313 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'filename'} = $esc_name; 314 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'artist'} = $taghash->{ARTIST} || $taghash->{'artist'}|| "N/A"; 315 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'album'} = $taghash->{ALBUM} || $taghash->{'album'} || "N/A"; 316 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'title'} = $taghash->{TITLE} || $taghash->{'title'} || "N/A"; 317 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration'} = sprintf("%02d:%02d", 318 | $infohash->{MM} || 0, $infohash->{SS} || 0); 319 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration_s'} = sprintf("%d", ($infohash->{MM} || 0) * 60 + ($infohash->{SS} || 0)); 320 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} = $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} || "0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0"; 321 | $num_songs++; 322 | } 323 | elsif ($mime_long =~ m/^Ogg Audio$/) { 324 | my $md5sum = md5_hex($File::Find::name); 325 | my $ogg = Ogg::Vorbis::Header::PurePerl->new($File::Find::name); 326 | my $esc_name = $File::Find::name; 327 | $esc_name =~ s/ /\\ /g; 328 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'filename'} = $esc_name; 329 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'artist'} = ($ogg->comment('artist'))[0] || ($ogg->comment('ARTIST'))[0] || "N/A"; 330 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'album'} = ($ogg->comment('album'))[0] || ($ogg->comment('ALBUM'))[0] || "N/A"; 331 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'title'} = ($ogg->comment('title'))[0] || ($ogg->comment('TITLE'))[0] || "N/A"; 332 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration'} = sprintf("%02d:%02d", int($ogg->info('length') / 60), int($ogg->info('length') % 60)); 333 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration_s'} = int($ogg->info('length')); 334 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} = $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} || "0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0"; 335 | $num_songs++; 336 | } 337 | elsif ($mime_long =~ m/^FLAC audio$/) { 338 | my $md5sum = md5_hex($File::Find::name); 339 | my $flac = Audio::FLAC::Header->new($File::Find::name); 340 | my $info = $flac->info(); 341 | my $tags = $flac->tags(); 342 | my $esc_name = $File::Find::name; 343 | $esc_name =~ s/ /\\ /g; 344 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'filename'} = $esc_name; 345 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'artist'} = $tags->{'ARTIST'} || $tags->{'artist'} || "N/A"; 346 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'album'} = $tags->{'ALBUM'} || $tags->{'album'} || "N/A"; 347 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'title'} = $tags->{'TITLE'} || $tags->{'title'} || "N/A"; 348 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration'} = sprintf("%02d:%02d", $flac->{'trackLengthMinutes'}, $flac->{'trackLengthSeconds'}); 349 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'duration_s'} = int($flac->{'trackTotalLengthSeconds'}); 350 | $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} = $STORAGE->{'PLAYLIST'}{'ALL'}{$md5sum}{'eq'} || "0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0:0.0"; 351 | $num_songs++; 352 | } 353 | } 354 | 355 | # func: add_dir 356 | # adds a new directory to the STORAGE hashref. 357 | sub add_dir { 358 | my $file = $cui->dirbrowser(); 359 | if (defined $file && -d $file && length $file) { 360 | unless (grep {/^$file$/} @{$STORAGE->{'DIRS'}}) { 361 | push @{$STORAGE->{'DIRS'}}, $file; 362 | &rescan_dirs(); 363 | } 364 | } 365 | } 366 | 367 | # rescan collection 368 | my $wait; 369 | sub rescan_dirs { 370 | $cui->status('\m/ ... scanning directories ... \m/'); 371 | $num_songs = 0; 372 | $STORAGE->{'PLAYLIST'}{'ALL'} = undef; 373 | if (scalar @{$STORAGE->{'DIRS'}}) { 374 | find(\&found_mp3, @{$STORAGE->{'DIRS'}}); 375 | } 376 | $cui->nostatus(); 377 | &store_data(); 378 | &draw_menu(); 379 | } 380 | 381 | # func: new_pl 382 | # creates a new playlist with an empty hashref ready for filling 383 | sub new_pl { 384 | $pl_add = $cui->question('NEW playlist: choose name (a-zA-Z0-9_)'); 385 | if (defined $pl_add) { 386 | if ($pl_add =~ m/^[a-zA-Z0-9_]+$/ && length $pl_add > 0 && $pl_add ne 'ALL') { 387 | unless (grep {/^$pl_add$/} keys %{$STORAGE->{'PLAYLIST'}}) { 388 | $STORAGE->{'PLAYLIST'}{$pl_add} = {}; 389 | } 390 | } 391 | } 392 | &store_data(); 393 | &draw_menu(); 394 | } 395 | 396 | # func: rem_dir 397 | # removes a music directory from the STORAGE hashref. 398 | sub rem_dir { 399 | my $rem = $win->add( 400 | 'rem', 'Listbox', 401 | -multi => 0, 402 | -y => int ($win->height() / 2) - 4, 403 | -x => int ($win->width() / 3) + 1, 404 | -focusable => 1, 405 | -height => 15, 406 | -width => 60, 407 | -border => 1, 408 | -ipad => 3, 409 | ); 410 | $rem->values(@{$STORAGE->{'DIRS'}}, 'CANCEL'); 411 | $rem->title('DELETE directory: choose dir (or cancel) and press TAB'); 412 | $rem->draw(); 413 | $rem->modalfocus(); 414 | my $selected = $rem->get(); 415 | $rem->loose_focus(); 416 | $win->delete('rem'); 417 | if ($selected) { 418 | if ($selected ne 'CANCEL') { 419 | my @new_dirs = grep {!/^$selected$/} @{$STORAGE->{'DIRS'}}; 420 | @{$STORAGE->{'DIRS'}} = @new_dirs; 421 | &rescan_dirs(); 422 | } 423 | } 424 | for my $plist (grep {!/^ALL$/} keys %{$STORAGE->{'PLAYLIST'}}) { 425 | for my $md (keys %{$STORAGE->{'PLAYLIST'}{$plist}}) { 426 | unless (grep {/$md/} keys %{$STORAGE->{'PLAYLIST'}{'ALL'}}) { 427 | delete $STORAGE->{'PLAYLIST'}{$plist}{$md}; 428 | } 429 | } 430 | } 431 | &draw_menu(); 432 | } 433 | 434 | # func: del_pl 435 | # deletes a playlist. the 'ALL' playlist must not be deleted! 436 | sub del_pl { 437 | my $del = $win->add( 438 | 'del', 'Listbox', 439 | -values => [grep {!/^ALL$/} keys %{$STORAGE->{'PLAYLIST'}}, 'CANCEL'], 440 | -multi => 0, 441 | -y => int ($win->height() / 2) - 4, 442 | -x => int ($win->width() / 3) + 1, 443 | -focusable => 1, 444 | -height => 15, 445 | -width => 60, 446 | -border => 1, 447 | -ipad => 3, 448 | ); 449 | $del->title('DELETE playlist: choose list (or cancel) and press TAB'); 450 | $del->draw(); 451 | $del->modalfocus(); 452 | my $pl_del = $del->get(); 453 | $del->loose_focus(); 454 | $win->delete('del'); 455 | if (defined $pl_del && exists $STORAGE->{'PLAYLIST'}{$pl_del}) { 456 | delete $STORAGE->{'PLAYLIST'}{$pl_del}; 457 | } 458 | &store_data(); 459 | &draw_menu(); 460 | } 461 | 462 | our ($art, $track, $file, $state, $cur_pos); 463 | $state = 'Stopped'; 464 | 465 | # plays the selected item or next/prev if given 466 | sub play { 467 | print $Min "stop\n"; 468 | $MAX_PL = (scalar (keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}})) - 1; 469 | return if $MAX_PL < 0; 470 | my $next = shift || ''; 471 | my ($index, $cur, $next_q); 472 | if ($next eq 'NEXT') { 473 | if (@QUEUE) { 474 | $LAST_PLAYED = (($curlist->id())[0] || "$LAST_PLAYED"); 475 | $next_q = shift @QUEUE; 476 | my $next_id = $Q2INDEX{$next_q}; 477 | delete $Q2INDEX{$next_q}; 478 | $curlist->clear_selection(); 479 | $curlist->set_selection($next_id); 480 | $index = $next_id; 481 | } 482 | else { 483 | $LAST_PLAYED = (($curlist->id())[0] || "$LAST_PLAYED"); 484 | $index = $SHUFFLE ? int(rand($MAX_PL)) : (($curlist->id())[0] || "$LAST_PLAYED") + 1; 485 | $index = $index > $MAX_PL ? 0 : $index; 486 | $curlist->clear_selection(); 487 | $curlist->set_selection($index); 488 | } 489 | $cur = ($curlist->get())[0]; 490 | $LAST_PLAYED = $index; 491 | } 492 | elsif ($next eq 'PREV') { 493 | $index = $SHUFFLE ? int(rand($MAX_PL)) : (($curlist->id())[0] || "$LAST_PLAYED") - 1; 494 | $LAST_PLAYED = $index; 495 | $index = $index < 0 ? $MAX_PL : $index; 496 | $curlist->clear_selection(); 497 | $curlist->set_selection(($LAST_PLAYED && $SHUFFLE) ? "$LAST_PLAYED" : $index); 498 | $cur = ($curlist->get())[0]; 499 | $LAST_PLAYED = ($curlist->id())[0] || "$LAST_PLAYED"; 500 | } 501 | else { 502 | $index = ($curlist->id())[0] || '0'; 503 | $curlist->set_selection($index); 504 | $cur = ($curlist->get())[0]; 505 | $curlist->clear_selection(); 506 | $curlist->set_selection($index); 507 | $LAST_PLAYED = $index; 508 | } 509 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$cur$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 510 | if (defined $msum) { 511 | $file = $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'filename'}; 512 | $track = $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'title'} || ''; 513 | $art = $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'artist'} || ''; 514 | $cur_pos = $index; 515 | } 516 | if ($file) { 517 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: Playing: " . $track . " by $art"); 518 | $curlist->clear_selection(); 519 | $curlist->draw(); 520 | print $Min "af equalizer=" . $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'eq'} . "\n"; 521 | @EQ = split /:/, $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'eq'}; 522 | $equalizer->text(sprintf("%4d %4d %4d %4d %4d\n", 1 .. 5) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[0..4]) . 523 | sprintf("%4d %4d %4d %4d %4d\n", 6 .. 10) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[5..9])); 524 | $equalizer->draw(); 525 | print $Min "loadfile $file\n"; 526 | my $altime = $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}{'duration_s'} || '0'; 527 | $TIMER->{'rest_time'} = $altime; 528 | $TIMER->{'max_time'} = $altime; 529 | alarm(1); 530 | } 531 | else { 532 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: ERROR - Could not find file for position $cur"); 533 | $curlist->clear_selection(); 534 | $curlist->draw(); 535 | } 536 | $state = 'Playing'; 537 | &draw_queue(); 538 | } 539 | 540 | # stop the current track, deactivate alarm 541 | sub stop { 542 | print $Min "stop\n"; 543 | alarm(0); 544 | $state = 'Stopped'; 545 | $TIMER->{'rest_time'} = 0; 546 | $TIMER->{'max_time'} = 0; 547 | $progressbar->title("--:-- / --:--"); 548 | $progressbar->pos(0); 549 | $progressbar->draw(); 550 | ($file, $track, $art) = (undef, undef, undef); 551 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state"); 552 | $curlist->draw(); 553 | } 554 | 555 | # pause the track, save alarm time, restart alarm if unpaused 556 | our $rest_altime; 557 | sub pause { 558 | if (defined $track) { 559 | print $Min "pause\n"; 560 | $STATE = $STATE ? 0 : 1; 561 | if ($STATE == 1) { 562 | $rest_altime = $TIMER->{'rest_time'}; 563 | alarm(0); 564 | $state = 'Paused'; 565 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state"); 566 | } 567 | else { 568 | $TIMER->{'rest_time'} = $rest_altime; 569 | alarm(1); 570 | $state = 'Playing'; 571 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state: $track by $art"); 572 | } 573 | } 574 | $curlist->draw(); 575 | } 576 | 577 | 578 | # add currently selected track to playlist 579 | sub add_to_pl { 580 | my @num = $curlist->get(); 581 | return unless @num; 582 | $sel_pl = $win->add( 583 | 'sel_pl', 'Listbox', 584 | -values => [grep {!/ALL/} keys %{$STORAGE->{'PLAYLIST'}}], 585 | -radio => 1, 586 | -y => int ($win->height() / 2), 587 | -x => int ($win->width() / 3) + 1, 588 | -focusable => 1, 589 | -height => 10, 590 | -width => 60, 591 | -border => 1, 592 | -ipad => 2, 593 | ); 594 | $sel_pl->title('ADD to playlist: choose playlist and press TAB'); 595 | $sel_pl->draw(); 596 | $sel_pl->modalfocus(); 597 | my $selected = $sel_pl->get(); 598 | $sel_pl->loose_focus(); 599 | $win->delete('sel_pl'); 600 | if (defined $selected) { 601 | for my $entry (@num) { 602 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$entry$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 603 | $STORAGE->{'PLAYLIST'}{$selected}{$msum} = $STORAGE->{'PLAYLIST'}{'ALL'}{$msum}; 604 | } 605 | } 606 | &store_data(); 607 | my $pltxt; 608 | $playlists->text(''); 609 | $playlists->draw(); 610 | $pltxt .= $CUR_LIST eq 'ALL' ? "[*] " : "[ ] "; 611 | $pltxt .= "ALL"; 612 | $pltxt .= sprintf(" (%d)\n", scalar keys %{$STORAGE->{'PLAYLIST'}{'ALL'}}); 613 | for (grep {!/ALL/} sort keys %{$STORAGE->{'PLAYLIST'}}) { 614 | $pltxt .= $CUR_LIST eq $_ ? "[*] " : "[ ] "; 615 | $pltxt .= "\'-> $_"; 616 | $pltxt .= sprintf(" (%d)\n", scalar keys %{$STORAGE->{'PLAYLIST'}{$_}}); 617 | } 618 | $playlists->text($pltxt); 619 | $playlists->draw(); 620 | } 621 | 622 | # switch playlist 623 | sub switch_pl { 624 | $sel_pl = $win->add( 625 | 'sel_pl', 'Listbox', 626 | -values => [keys %{$STORAGE->{'PLAYLIST'}}], 627 | -radio => 1, 628 | -y => int ($win->height() / 2), 629 | -x => int ($win->width() / 3) + 1, 630 | -focusable => 1, 631 | -height => 10, 632 | -width => 60, 633 | -border => 1, 634 | -ipad => 2, 635 | ); 636 | $sel_pl->title('SWITCH playlist: choose and press TAB'); 637 | $sel_pl->draw(); 638 | $sel_pl->modalfocus(); 639 | my $selected = $sel_pl->get(); 640 | $sel_pl->loose_focus(); 641 | $win->delete('sel_pl'); 642 | $CUR_LIST = $selected || 'ALL'; 643 | if ($state ne 'Playing') { 644 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state"); 645 | } 646 | else { 647 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state: $track by $art"); 648 | } 649 | &draw_menu(); 650 | } 651 | 652 | # play next item in list 653 | sub next { &play('NEXT'); } 654 | 655 | # play previous item in list 656 | sub prev { &play('PREV'); } 657 | 658 | # toggle shuffle, 1 = shuffle, 0 = normal playlist order 659 | sub toggle_shuffle { 660 | $SHUFFLE = $SHUFFLE ? 0 : 1; 661 | if ($state ne 'Playing') { 662 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state"); 663 | } 664 | else { 665 | $curlist->title("Playlist: $CUR_LIST ( S: $SHUFFLE ) :: $state: $track by $art"); 666 | } 667 | $curlist->draw(); 668 | } 669 | 670 | # seek 10 seconds forward in music stream 671 | sub seek_right { 672 | print $Min "seek 10\n"; 673 | my $newal = $TIMER->{'rest_time'}; 674 | if ($newal > 10) { 675 | $newal -= 10 ; 676 | } 677 | else { $newal = 1; } 678 | $TIMER->{'rest_time'} = $newal; 679 | } 680 | 681 | # seek 10 seconds back in music stream 682 | sub seek_left { 683 | my $newal = $TIMER->{'rest_time'}; 684 | if ($newal > 10) { 685 | print $Min "seek -10\n"; 686 | $newal += 10; 687 | } 688 | $TIMER->{'rest_time'} = $newal; 689 | } 690 | 691 | # delete the currently selected item from the currently selected playlist 692 | sub del_from_pl { 693 | my @to_del = $curlist->get(); 694 | return unless @to_del; 695 | for my $del (@to_del) { 696 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$del$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 697 | if (exists $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}) { 698 | delete $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$msum}; 699 | } 700 | } 701 | &draw_menu(); 702 | &store_data(); 703 | } 704 | 705 | sub get_lyrics { 706 | my $search = "$art $track"; 707 | my $bkupurl = "http://www.elyrics.net/find.php"; 708 | my $url = "http://search.azlyrics.com/search.php?q=$search"; 709 | my $ua = LWP::UserAgent->new(); 710 | my $response = $ua->get( $url); 711 | my $content = $response->decoded_content(); 712 | 713 | my $link; 714 | my $final; 715 | 716 | if ($content =~ m/1\. get("$link"); 721 | $content = $response->decoded_content(); 722 | 723 | if ($content =~ m/.+
.+?(.+?)<\/div>.+/gms) { 724 | $final = "Lyrics from azlyrics.com\n"; 725 | $final .= $1; 726 | } 727 | } 728 | unless ($final) { 729 | # let's try again 730 | my $response = $ua->post( $bkupurl, { 'q' => $search } ); 731 | my $content = $response->decoded_content(); 732 | 733 | if ($content =~ m/.+get("http://www.elyrics.net$link"); 738 | $content = $response->decoded_content(); 739 | 740 | if ($content =~ m/.+
(.+?)
.+/gms) { 741 | $final = "Lyrics from elyrics.net\n\n"; 742 | $final .= $1; 743 | } 744 | } 745 | } 746 | unless ($final) { 747 | $final = "not found"; 748 | } 749 | $final =~ s/
//gms; 750 | $lyrics->title("$art - $track"); 751 | $lyrics->text($final); 752 | $lyrics->draw(); 753 | } 754 | 755 | 756 | 757 | sub band_switch { 758 | my ($band) = @_; 759 | my @freq = ("31.25Hz", "62.50Hz", "125.0Hz", "250.0Hz", "500.0Hz", 760 | "1.0kHz", "2.0kHz", "4.0kHz", "8.0kHz", "16.0kHz"); 761 | 762 | my $new_val = $cui->question("Please enter the new value for freq: $freq[$band] (currently $EQ[$band]dB)"); 763 | $EQ[$band] = sprintf ("%4.1f", $new_val) if ((defined $new_val && $new_val == 0 ) || (defined $new_val && $new_val <= 12.0 && $new_val >= -12.0)); 764 | $equalizer->text(sprintf("%4d %4d %4d %4d %4d\n", 1 .. 5) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[0..4]) . 765 | sprintf("%4d %4d %4d %4d %4d\n", 6 .. 10) . sprintf("%4.1f %4.1f %4.1f %4.1f %4.1f\n", @EQ[5..9])); 766 | $equalizer->draw(); 767 | my $eq_str = join ":", @EQ; 768 | print $Min "af equalizer=$eq_str\n"; 769 | } 770 | 771 | sub b1_switch { &band_switch(0); } 772 | sub b2_switch { &band_switch(1); } 773 | sub b3_switch { &band_switch(2); } 774 | sub b4_switch { &band_switch(3); } 775 | sub b5_switch { &band_switch(4); } 776 | sub b6_switch { &band_switch(5); } 777 | sub b7_switch { &band_switch(6); } 778 | sub b8_switch { &band_switch(7); } 779 | sub b9_switch { &band_switch(8); } 780 | sub b10_switch { &band_switch(9); } 781 | 782 | sub help { 783 | $cui->dialog("P - play selected song\n" . "L - show lyrics\n" . 784 | "p - pause\n" . "n - play next song\n" . "r - play previous song\n" . 785 | "j - jump to currently playing song\n" . "+ - add to queue\n" . 786 | "- - remove from queue\n" . 787 | "S - stop playing\n". "t - toggle shuffle\n" . "e - rescan directories\n" . 788 | "Q - quit\n\n" . "a - add to playlist\n" . "d - delete from playlist\n" . 789 | "s - switch to playlist\n" . "A - add directory\n" . "R - remove directory\n" . 790 | "N - new playlist\n" . "D - delete playlist\n\n" . 791 | "v - save equalizer for artist/album/song\n\n" . 792 | "SPACE or ENTER to select, TAB to confirm\nLEFT / RIGHT Arrow Key seek +-10s" 793 | ); 794 | } 795 | 796 | # save equalizer settings 797 | sub save_eq { 798 | my @songs = $curlist->get(); 799 | my %choo = ( 1 => 'selected songs only', 2 => 'all songs on this album', 3 => 'all songs from this artist' ); 800 | my $choose = $win->add( 801 | 'sel_pl', 'Listbox', 802 | -values => [""], 803 | -radio => 1, 804 | -y => int ($win->height() / 2), 805 | -x => int ($win->width() / 3) + 1, 806 | -focusable => 1, 807 | -height => 9, 808 | -width => 60, 809 | -border => 1, 810 | -ipad => 2, 811 | ); 812 | $choose->title('SAVE eq settings for: (press TAB afterwards):'); 813 | 814 | $choose->values(sort keys %choo); 815 | $choose->labels(\%choo); 816 | $choose->draw(); 817 | $choose->modalfocus(); 818 | my $selected = $choose->get(); 819 | $choose->loose_focus(); 820 | $win->delete('sel_pl'); 821 | if (defined $selected) { 822 | if ($selected == 1) { 823 | for my $song (@songs) { 824 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$song$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 825 | $STORAGE->{'PLAYLIST'}{'ALL'}{$msum}{'eq'} = join ":", @EQ; 826 | } 827 | } 828 | elsif ($selected == 2) { 829 | for my $song (@songs) { 830 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$song$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 831 | my $album = $STORAGE->{'PLAYLIST'}{'ALL'}{$msum}{'album'}; 832 | my $artist = $STORAGE->{'PLAYLIST'}{'ALL'}{$msum}{'artist'}; 833 | for my $k (keys %{$STORAGE->{'PLAYLIST'}{'ALL'}}) { 834 | if ($STORAGE->{'PLAYLIST'}{'ALL'}{$k}{'album'} eq $album && $STORAGE->{'PLAYLIST'}{'ALL'}{$k}{'artist'} eq $artist) { 835 | $STORAGE->{'PLAYLIST'}{'ALL'}{$k}{'eq'} = join ":", @EQ; 836 | } 837 | } 838 | } 839 | } 840 | elsif ($selected == 3) { 841 | for my $song (@songs) { 842 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$song$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 843 | my $artist = $STORAGE->{'PLAYLIST'}{'ALL'}{$msum}{'artist'}; 844 | for my $k (keys %{$STORAGE->{'PLAYLIST'}{'ALL'}}) { 845 | if ($STORAGE->{'PLAYLIST'}{'ALL'}{$k}{'artist'} eq $artist) { 846 | $STORAGE->{'PLAYLIST'}{'ALL'}{$k}{'eq'} = join ":", @EQ; 847 | } 848 | } 849 | } 850 | } 851 | } 852 | &store_data(); 853 | &draw_menu(); 854 | } 855 | 856 | 857 | sub jump { 858 | if (defined $file && $cur_pos <= $curlist->{-max_selected}) { 859 | $curlist->{-ypos} = $cur_pos; 860 | $curlist->draw(); 861 | } 862 | } 863 | 864 | sub enqueue { 865 | my $id = ($curlist->get())[0]; 866 | return unless defined $id; 867 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$id$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 868 | return unless $msum; 869 | for my $entry (@QUEUE) { 870 | return if $entry eq $msum; 871 | } 872 | push @QUEUE, $msum; 873 | $Q2INDEX{$msum} = $id; 874 | &draw_queue(); 875 | $curlist->clear_selection(); 876 | $curlist->draw(); 877 | } 878 | sub dequeue { 879 | my $id = ($curlist->get())[0]; 880 | return unless defined $id; 881 | my ($msum) = grep { $STORAGE->{'PLAYLIST'}{$CUR_LIST}{$_}{'index'} =~ /^$id$/ } keys %{$STORAGE->{'PLAYLIST'}{$CUR_LIST}}; 882 | return unless $msum; 883 | my @NEW_Q; 884 | for my $entry (@QUEUE) { 885 | push @NEW_Q, $entry unless $entry eq $msum; 886 | } 887 | @QUEUE = @NEW_Q; 888 | delete $Q2INDEX{$msum} if exists $Q2INDEX{$msum}; 889 | &draw_queue(); 890 | $curlist->clear_selection(); 891 | $curlist->draw(); 892 | } 893 | 894 | 895 | 896 | # define keybindings, draw menu for first time, start main loop :) 897 | $cui->set_binding(\&help, "h"); 898 | $cui->set_binding(\&quit_program, "Q"); 899 | $cui->set_binding(\&enqueue, "+"); 900 | $cui->set_binding(\&dequeue, "-"); 901 | $cui->set_binding(\&add_dir, "A"); 902 | $cui->set_binding(\&rem_dir, "R"); 903 | $cui->set_binding(\&del_pl, "D"); 904 | $cui->set_binding(\&new_pl, "N"); 905 | $cui->set_binding(\&play, "P" ); 906 | $cui->set_binding(\&pause, "p" ); 907 | $cui->set_binding(\&add_to_pl, "a" ); 908 | $cui->set_binding(\&del_from_pl, "d" ); 909 | $cui->set_binding(\&switch_pl, "s" ); 910 | $cui->set_binding(\&next, "n" ); 911 | $cui->set_binding(\&prev, "r" ); 912 | $cui->set_binding(\&toggle_shuffle, "t" ); 913 | $cui->set_binding(\&stop, "S" ); 914 | $cui->set_binding(\&rescan_dirs, "e" ); 915 | $cui->set_binding(\&save_eq, "v" ); 916 | $cui->set_binding(\&jump, "j" ); 917 | $cui->set_binding(\&get_lyrics, "L" ); 918 | 919 | $cui->set_binding(\&seek_right, KEY_RIGHT ); 920 | $cui->set_binding(\&seek_left, KEY_LEFT ); 921 | 922 | $cui->set_binding(\&b1_switch, KEY_F(1) ); 923 | $cui->set_binding(\&b2_switch, KEY_F(2) ); 924 | $cui->set_binding(\&b3_switch, KEY_F(3) ); 925 | $cui->set_binding(\&b4_switch, KEY_F(4) ); 926 | $cui->set_binding(\&b5_switch, KEY_F(5) ); 927 | $cui->set_binding(\&b6_switch, KEY_F(6) ); 928 | $cui->set_binding(\&b7_switch, KEY_F(7) ); 929 | $cui->set_binding(\&b8_switch, KEY_F(8) ); 930 | $cui->set_binding(\&b9_switch, KEY_F(9) ); 931 | $cui->set_binding(\&b10_switch, KEY_F(10) ); 932 | 933 | &draw_menu(); 934 | $cui->mainloop(); 935 | --------------------------------------------------------------------------------