X-Git-Url: https://git.draconx.ca/gitweb/mpdhacks.git/blobdiff_plain/020f9f800a804c638d6432711be319c5b9bfb988..HEAD:/mpdmenu.pl diff --git a/mpdmenu.pl b/mpdmenu.pl index 5968843..71613c5 100755 --- a/mpdmenu.pl +++ b/mpdmenu.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl # -# Copyright © 2008,2010,2012,2019 Nick Bowler +# Copyright © 2008,2010,2012,2020-2021 Nick Bowler # # Silly little script to generate an FVWM menu with various bits of MPD # status information and controls. @@ -18,12 +18,14 @@ use Encode::Locale qw(decode_argv); decode_argv(Encode::FB_CROAK); binmode(STDOUT, ":utf8"); -use IO::Socket::INET6; use Getopt::Long qw(:config gnu_getopt); use Scalar::Util qw(reftype); use List::Util qw(any max); use FindBin; +use lib "$FindBin::Bin"; +use MPDHacks; + use constant { MPD_MJR_MIN => 0, MPD_MNR_MIN => 21, @@ -33,48 +35,19 @@ use constant { my $SELF = "$FindBin::Bin/$FindBin::Script"; my $MUSIC = $ENV{MUSIC} // "/srv/music"; -my $host = $ENV{MPD_HOST} // "localhost"; -my $port = $ENV{MPD_PORT} // "6600"; -my $sock; +my ($sock, $mpd_have_binarylimit); -my ($albumid, $title); -my %artistids; -my $menu; +my ($albumid, $albumname, $trackid, $recordingid); +my ($topmenu, $menu); my $mode = "top"; - -# Quotes the argument so that it is presented as a single argument to MPD -# at the protocol level. This also works OK for most FVWM arguments. -sub escape { - my $s = @_[0] // $_; - - # No way to encode literal newlines in the protocol, so we - # convert any newlines in the arguments into a space, which - # can help with quoting. - $s =~ s/\n/ /g; - - if (/[ \t\\"]/) { - $s =~ s/[\\"]/\\$&/g; - return "\"$s\""; - } - - $s =~ s/^\s*$/"$&"/; - return $s; -} - -# Submit a command to the MPD server; each argument to this function -# is quoted and sent as a single argument to MPD. -sub mpd_exec { - my $cmd = join(' ', map { escape } @_); - - print $sock "$cmd\n"; -} +my %artistids; sub fvwm_cmd_unquoted { print join(' ', @_), "\n"; } sub fvwm_cmd { - fvwm_cmd_unquoted(map { escape } @_); + fvwm_cmd_unquoted(map { MPD::escape } @_); } # Quotes the argument in such a way that it is passed unadulterated by @@ -112,7 +85,7 @@ sub fvwm_label_escape { sub make_submenu { my $name = shift; $name =~ s/-/_/g; - unshift @_, ("exec", $SELF, "--menu=$name"); + unshift @_, ("exec", $SELF, "--topmenu=$topmenu", "--menu=$name"); fvwm_cmd("DestroyFunc", "Make$name"); fvwm_cmd("AddToFunc", "Make$name"); @@ -120,14 +93,14 @@ sub make_submenu { fvwm_cmd("DestroyMenu", $name); fvwm_cmd("AddToMenu", $name, "DynamicPopupAction", "Make$name"); - fvwm_cmd("AddToFunc", "KillMenuMPD", "I", "DestroyMenu", $name); + fvwm_cmd("AddToFunc", "Kill$topmenu", "I", "DestroyMenu", $name); fvwm_cmd("DestroyFunc", "Make$name"); fvwm_cmd("AddToFunc", "Make$name"); fvwm_cmd("+", "I", "DestroyMenu", $name); fvwm_cmd("+", "I", "-PipeRead", join(' ', map { fvwm_shell_literal } @_)); - fvwm_cmd("AddToFunc", "KillMenuMPD", "I", "DestroyFunc", "Make$name"); + fvwm_cmd("AddToFunc", "Kill$topmenu", "I", "DestroyFunc", "Make$name"); return ("Popup", $name); } @@ -161,6 +134,13 @@ sub get_item_thumbnails { $c = "%"; } + if ($mpd_have_binarylimit) { + # --embedded implies and requires binarylimit support + push @opts, "--embedded"; + } else { + push @opts, "--no-binarylimit"; + } + open THUMB, "-|", "$FindBin::Bin/mpdthumb.sh", @opts, "--", @_; foreach (@_) { my $thumb = ; @@ -243,8 +223,8 @@ sub top_track_cover { my $cover = mpd_cover_filename($file); $cover = fvwm_shell_literal($cover // $file); - fvwm_cmd_unquoted("AddToMenu", escape($menu), - escape($entry->{thumb}), + fvwm_cmd_unquoted("AddToMenu", MPD::escape($menu), + MPD::escape($entry->{thumb}), "Exec", "exec", "geeqie", $cover); } } @@ -252,9 +232,16 @@ sub top_track_cover { # Generate the "Title:" entry in the top menu. sub top_track_title { my ($entry) = @_; + my @submenu; - my @submenu = make_submenu("$menu-TopTrack", - "--title=$entry->{Title}"); + my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID"); + if ($mbid) { + @submenu = make_submenu("$menu-$mbid", "--track-id=$mbid") + } else { + ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID"); + @submenu = make_submenu("$menu-track-$mbid", "--recording-id=$mbid") + if ($mbid); + } fvwm_cmd("AddToMenu", $menu, fvwm_label_escape("Title:\t$entry->{Title}"), @@ -283,15 +270,142 @@ sub top_track_artist { sub top_track_album { my ($entry) = @_; my @submenu; + my $mbid; - my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID"); - @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid") if $mbid; + if (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID")) { + @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid"); + } elsif (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID")) { + # Standalone recording + my @a = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID"); + my ($album) = get_track_metadata($entry, "Album"); + + @submenu = make_submenu("$menu-$mbid", "--album-name=$album", + map { "--artist-id=$_" } @a); + + } fvwm_cmd("AddToMenu", $menu, fvwm_label_escape("Album:\t$entry->{Album}"), @submenu); } +# Generate the "MusicBrainz:" entry in the top menu. +sub top_track_musicbrainz { + my ($entry) = @_; + my ($track_mbid, $recording_mbid, $release_mbid); + my @artist_mbids; + my $label = "MB:"; + my %idmap; + + ($track_mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID"); + ($recording_mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID"); + ($release_mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID"); + @artist_mbids = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID"); + return unless $track_mbid // $recording_mbid + // $release_mbid // @artist_mbids; + + foreach (get_track_metadata($entry, "Comment")) { + $idmap{$1} = $2 if /^([^=]*)=(.*) \(idmap\)$/; + } + + fvwm_cmd("AddToMenu", $menu, "", "Nop"); + if ($track_mbid) { + fvwm_cmd("AddToMenu", $menu, "$label\tShow track", + "Exec", "exec", "xdg-open", + "https://musicbrainz.org/track/$track_mbid"); + $label = ""; + } elsif ($recording_mbid) { + fvwm_cmd("AddToMenu", $menu, "$label\tShow recording", + "Exec", "exec", "xdg-open", + "https://musicbrainz.org/recording/$recording_mbid"); + $label = ""; + } elsif ($release_mbid) { + fvwm_cmd("AddToMenu", $menu, "$label\tShow", + "Exec", "exec", "xdg-open", + "https://musicbrainz.org/release/$release_mbid"); + $label = ""; + } + + foreach my $mbid (@artist_mbids) { + my $name = " $idmap{$mbid}" if $idmap{$mbid}; + + fvwm_cmd("AddToMenu", $menu, "$label\tShow artist$name", + "Exec", "exec", "xdg-open", + "https://musicbrainz.org/artist/$mbid"); + $label = ""; + } +} + +# Given a work MBID, return a hash reference containing all tracks +# linked to that work. The hash keys are filenames. +sub get_tracks_by_work_mbid { + my %matches; + my $entry; + + foreach my $mbid (@_) { + MPD::exec("search", "(MUSICBRAINZ_WORKID == \"$mbid\")"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + if ($1 eq "file") { + if (exists($matches{$2})) { + $entry = $matches{$2}; + } else { + $entry = {}; + $matches{$2} = $entry; + } + } + + add_track_metadata($entry, $1, $2); + } + } + } + + return \%matches; +} + +# Given a track MBID, return a hash reference containing all "related" +# tracks in the MPD database. The hash keys are filenames. +# +# Currently tracks are considered "related" if their associated recordings +# have at least one work in common. +sub get_tracks_by_track_mbid { + my ($mbid, $tagname) = (@_, "MUSICBRAINZ_RELEASETRACKID"); + my %source; + my %matches; + my $entry; + + return \%matches unless ($mbid); + MPD::exec("search", "($tagname == \"$mbid\")"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + add_track_metadata(\%source, $1, $2); + } + } + + # Always include the current track + $matches{$source{file}} = \%source; + + # Find all tracks related by work + foreach my $mbid (get_track_metadata(\%source, "MUSICBRAINZ_WORKID")) { + my $related = get_tracks_by_work_mbid($mbid); + foreach (keys %$related) { + $matches{$_} //= $related->{$_}; + } + } + + return \%matches; +} + +sub get_tracks_by_recording_mbid { + return get_tracks_by_track_mbid($_[0], "MUSICBRAINZ_TRACKID"); +} + # Given a release MBID, return a hash reference containing all its # associated tracks in the MPD database. The hash keys are filenames. sub get_tracks_by_release_mbid { @@ -300,7 +414,7 @@ sub get_tracks_by_release_mbid { my $entry; return \%matches unless ($mbid); - mpd_exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")"); + MPD::exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")"); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); @@ -322,23 +436,39 @@ sub get_tracks_by_release_mbid { return \%matches; } -# Given an artist MBID, return a hash reference containing associated -# releases in the MPD database. The hash keys are release MBIDs. +# Insert the given entry into the referenced hash if it represents a +# standalone recording (not associated with a release). The recording +# MBID is used as the hash key. +sub check_standalone { + my ($outhash, $entry) = @_; + my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID"); + + return if exists $entry->{MUSICBRAINZ_ALBUMID}; + $outhash->{$mbid} = $entry if ($mbid); +} + +# Given an artist MBID, return a list of two hash refererences. The +# first contains the associated releases in the MPD database and the +# hash keys are release MBIDs. The second contains the artist's +# standalone recordings and the hash keys are recording MBIDs. +# +# In scalar context only the release hash is returned. # # Since MPD returns results on a per-track basis, each entry in the # hash has the metadata for one unspecified track from that release. sub get_releases_by_artist_mbid { - my %releases; + my (%releases, %standalones); my $entry; foreach my $mbid (@_) { - mpd_exec("search", "(MUSICBRAINZ_ARTISTID == \"$mbid\")"); + MPD::exec("search", "(MUSICBRAINZ_ARTISTID == \"$mbid\")"); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); if (/^(\w+): (.*)$/) { if ($1 eq "file") { + check_standalone(\%standalones, $entry); $entry = {}; } elsif ($1 eq "MUSICBRAINZ_ALBUMID") { $releases{$2} //= $entry; @@ -347,9 +477,10 @@ sub get_releases_by_artist_mbid { add_track_metadata($entry, $1, $2); } } + check_standalone(\%standalones, $entry); } - return \%releases; + return wantarray ? (\%releases, values %standalones) : \%releases; } # Given a filename, return the IDs (if any) for that file in the @@ -358,7 +489,7 @@ sub get_ids_by_filename { my ($file) = @_; my @results = (); - mpd_exec("playlistfind", "file", $file); + MPD::exec("playlistfind", "file", $file); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); @@ -371,6 +502,24 @@ sub get_ids_by_filename { return @results; } +sub update_entry_ids { + my @notqueued = (); + + foreach my $entry (@_) { + unless (exists $entry->{Id}) { + my ($id) = get_ids_by_filename($entry->{file}); + if (defined $id) { + $entry->{Id} = $id; + } else { + push @notqueued, $entry; + next; + } + } + } + + return @notqueued; +} + # albumsort(matches, a, b) # # Sort hash keys (a, b) by disc/track number for album menus. @@ -404,8 +553,8 @@ sub menu_trackname { sub print_version { print < 0); + print $fh "Try $0 --help for more information.\n" unless (@_ > 0); } sub print_help { @@ -429,41 +578,53 @@ Options: -p, --port=PORT Connect to the MPD server on PORT, overriding defaults. -m, --menu=NAME Set the name of the generated menu. --album-id=MBID Generate a menu for the given release MBID. + --album-name=NAME + Generate a menu for standalone tracks with the given + "album" NAME. An artist MBID must be supplied. --artist-id=MBID Generate a menu for the given artist MBID. + --track-id=MBID Generate a menu for the given track MBID. + --recording-id=MBID + Generate a menu for the given recording MBID. -V, --version Print a version message and then exit. -H, --help Print this message and then exit. EOF } GetOptions( - 'host|h=s' => \$host, - 'port|p=s' => \$port, - 'menu|m=s' => \$menu, + 'host|h=s' => \$MPD::host, + 'port|p=s' => \$MPD::port, + 'menu|m=s' => \$menu, + + 'artist-id=s' => sub { $artistids{$_[1]} = 1; $mode = "artist"; }, + 'album-id=s' => sub { $albumid = $_[1]; $mode = "album"; }, + 'album-name=s' => sub { $albumname = $_[1]; $mode = "albumname"; }, + 'track-id=s' => sub { $trackid = $_[1]; $mode = "track"; }, + 'recording-id=s' => sub { $recordingid = $_[1]; $mode = "recording"; }, - 'artist-id=s' => sub { $artistids{$_[1]} = 1; $mode = "artist"; }, - 'album-id=s' => sub { $albumid = $_[1]; $mode = "album"; }, - 'title=s' => sub { $title = $_[1]; $mode = "track"; }, + 'V|version' => sub { print_version(); exit }, + 'H|help' => sub { print_help(); exit }, - 'V|version' => sub { print_version(); exit }, - 'H|help' => sub { print_help(); exit }, + 'topmenu=s' => \$topmenu, # top menu name (for submenu generation) ) or do { print_usage; exit 1 }; +$mode = "albumname" if ($albumname && $mode eq "artist"); + +unless (defined $menu) { + $topmenu //= "MenuMPD"; + $menu = $topmenu . ($mode ne "top" ? $mode : ""); +} +$topmenu //= $menu; + # Connect to MPD. -$sock = new IO::Socket::INET6( - PeerAddr => $host, - PeerPort => $port, - Proto => 'tcp', - Timeout => 2 -) or die("could not open socket: $!.\n"); -binmode($sock, ":utf8"); - -die("could not connect to MPD: $!.\n") - if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)); - -die("MPD version $1.$2.$3 insufficient.\n") - if ( ($1 < MPD_MJR_MIN) - || ($1 == MPD_MJR_MIN && $2 < MPD_MNR_MIN) - || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN)); +$sock = MPD::connect(); +die("MPD version $MPD::major.$MPD::minor.$MPD::revision insufficient.") + unless MPD::min_version(MPD_MJR_MIN, MPD_MNR_MIN, MPD_REV_MIN); + +MPD::exec("binarylimit", 64); +while (<$sock>) { + $mpd_have_binarylimit = 1 if /^OK/; + last if /^OK/ or /^ACK/; +} if ($mode eq "top") { my %current; @@ -471,7 +632,7 @@ if ($mode eq "top") { $menu //= "MenuMPD"; - mpd_exec("status"); + MPD::exec("status"); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); @@ -481,7 +642,7 @@ if ($mode eq "top") { } } - mpd_exec("currentsong"); + MPD::exec("currentsong"); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); @@ -502,6 +663,7 @@ if ($mode eq "top") { top_track_title(\%current); top_track_artist(\%current); top_track_album(\%current); + top_track_musicbrainz(\%current); } else { fvwm_cmd("AddToMenu", $menu, "[current track unavailable]"); } @@ -576,134 +738,111 @@ if ($mode eq "top") { } } elsif ($mode eq "artist") { # Create an artist menu. - my $matches = get_releases_by_artist_mbid(keys %artistids); - my $entry; + my ($matches, @recs) = get_releases_by_artist_mbid(keys %artistids); $menu //= "MenuMPDArtist"; my @mbids = sort { datesort($matches, $a, $b) } keys %$matches; my @files = map { $matches->{$_}->{file} } @mbids; my @thumbs = get_item_thumbnails({ small => 1 }, @files); - fvwm_cmd("AddToMenu", $menu, "No releases found", "Title") unless @mbids; + + unless (@mbids) { + fvwm_cmd("AddToMenu", $menu, "No releases found", "Title") + } foreach my $mbid (@mbids) { my $entry = $matches->{$mbid}; my $thumb = shift @thumbs; - my @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid"); + my @submenu = make_submenu("$topmenu-$mbid", + "--album-id=$mbid"); fvwm_cmd("AddToMenu", $menu, $thumb . fvwm_label_escape($entry->{Album}), - @submenu); + @submenu); } -} elsif ($mode eq "track") { - # Create a title menu. - my @titles; - my $entry; - $menu = "MenuMPDTitle" unless defined $menu; + my @artists = map { "--artist-id=$_" } keys %artistids; + my %nonalbums = map { $_->{Album} => $_ } @recs; + foreach my $name (sort keys %nonalbums) { + my $mbid = $nonalbums{$name}->{MUSICBRAINZ_TRACKID}; + my @submenu = make_submenu("$topmenu-$mbid", @artists, + "--album-name=$name"); + fvwm_cmd("AddToMenu", $menu, fvwm_label_escape($name), @submenu); + } +} elsif ($mode eq "albumname") { + # Create a standalone recordings menu + my ($releases, @recs) = get_releases_by_artist_mbid(keys %artistids); + + $menu //= "MenuMPDRecordings"; + my @tracks = sort { $a->{Title} cmp $b->{Title} } + grep { $_->{Album} eq $albumname } @recs; - # Open and close brackets. - my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]"); + # Show thumbnails for standalone recordings + my @thumbs = get_item_thumbnails({ small => 1 }, + map { $_->{file} } @tracks); + foreach my $entry (@tracks) { + $entry->{thumb} = shift @thumbs; + } - $_ = $title; + my @notqueued = update_entry_ids(@tracks); - # Deal with specific cases. - s/ちいさな(?=ヘミソフィア)//; # ヘミソフィア - s/ "mix on air flavor" dear EIKO SHIMAMIYA//; # Spiral wind - s/ "So,you need me" Style//; # I need you - s/ ::Symphony Second movement:://; # Disintegration - s/-\[instrumental\]//; # 青い果実 - s/ -Practice Track-//; # Fair Heaven - s/〜世界で一番アナタが好き〜//; # Pure Heart - s/〜彼方への哀歌//; # 十二幻夢 - s/ sora no uta ver.//; # 美しい星 + fvwm_cmd("AddToMenu", $menu); + fvwm_cmd("+", "No tracks found", "Title") unless @tracks; - s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right. + foreach my $entry (@tracks) { + next unless exists $entry->{Id}; - # Deal with titles like "blah (ABC version)". - s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i; + fvwm_cmd("+", menu_trackname($entry), "Exec", + "exec", "$FindBin::Bin/mpdexec.pl", + "playid", $entry->{Id}); + } - # Deal with titles like "blah (without XYZ)". - s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i; + fvwm_cmd("+", "Not in play queue", "Title") if @notqueued; + foreach my $entry (@notqueued) { + fvwm_cmd("+", menu_trackname($entry)); + } +} elsif ($mode eq "track" || $mode eq "recording") { + my ($matches, $mbid); + my @notqueued; - # Deal with titles like "blah instrumental". - s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i; - s/\s+without\s+\w+$//i; + if ($mode eq "track") { + $matches = get_tracks_by_track_mbid($trackid) + } else { + $matches = get_tracks_by_recording_mbid($recordingid) + } - # Deal with separate movements in classical pieces. - s/: [IVX]+\..*//; + $menu //= "MenuMPDTrack"; + fvwm_cmd("DestroyMenu", $menu); - my $basetitle = $_; - my $_basetitle = $basetitle; + my @files = sort { datesort($matches, $a, $b) } keys %$matches; + my @thumbs = get_item_thumbnails({ small => 1 }, @files); - $_basetitle =~ s/"/\\"/g; - print $sock "playlistsearch title \"$_basetitle\"\n"; - while (<$sock>) { - last if (/^OK/); - die($_) if (/^ACK/); + fvwm_cmd("AddToMenu", $menu); + fvwm_cmd("+", "No tracks found", "Title") unless @files; + foreach my $file (@files) { + my $entry = $matches->{$file}; + $entry->{thumb} = shift @thumbs; - if (/^(\w+): (.*)$/) { - if ($1 eq "file") { - push @titles, $entry if (keys(%$entry) > 0); - $entry = {}; + unless (exists $entry->{Id}) { + my ($id) = get_ids_by_filename($file); + if (defined $id) { + $entry->{Id} = $id; + } else { + push @notqueued, $entry; + next; } - - $entry->{$1} = $2; } - } - push @titles, $entry if (keys(%$entry) > 0); - -{ # work around 'use locale' breaking s///i - use locale; - my @thumbs = get_item_thumbnails({ small => 1 }, - map { $_->{file} } @titles); - for (my $i = 0; $i < @titles; $i++) { - $titles[$i]->{thumb} = $thumbs[$i]; + fvwm_cmd("+", menu_trackname($entry), "Exec", + "exec", "$FindBin::Bin/mpdexec.pl", + "playid", $entry->{Id}); } - foreach (sort titlesort @titles) { - my ($t_file, $t_artist, $t_title, $t_id, $thumb) = ( - $_->{file}, - $_->{Artist}, - $_->{Title}, - $_->{Id}, - $_->{thumb} - ); - - # MPD searches are case-insensitive. - next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i)); - - $t_artist = sanitise($t_artist, 1); - $t_title = sanitise($t_title, 1); - - cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\"" - ." Exec exec $FindBin::Bin/mpdexec.pl" - ." playid $t_id"); + fvwm_cmd("+", "Not in play queue", "Title") if @notqueued; + foreach my $entry (@notqueued) { + fvwm_cmd("+", menu_trackname($entry)); } -} # end use locale workaround } # Finished. print $sock "close\n"; - -sub sanitise -{ - $_ = $_[0]; - s/&/&&/g if ($_[1]); - s/([\$@%^*])/\1\1/g; - s/"/\\"/g; - return $_; -} - -sub titlesort -{ - return ($a->{Album} cmp $b->{Album}) if($a->{Album} ne $b->{Album}); - return ($a->{Artist} cmp $b->{Artist}) if($a->{Artist} ne $b->{Artist}); - return ($a->{Title} cmp $b->{Title}); -} - -sub cmd -{ - print "$_[0]\n"; -}