Skip to content

Instantly share code, notes, and snippets.

@benjacoblee
Created November 1, 2023 13:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjacoblee/7a366f6b0c912f0dd754673e0cd1edf5 to your computer and use it in GitHub Desktop.
Save benjacoblee/7a366f6b0c912f0dd754673e0cd1edf5 to your computer and use it in GitHub Desktop.
#!/home/benn/.asdf/shims/raku
use URI;
use URI::Escape;
use HTTP::Tiny;
use DOM::Tiny;
our $BASEDIR = "$*HOME/.lrix"; # $*HOME is a special variable
our $BASEURL = "https://www.musixmatch.com";
our $LYRICS_NOT_AVAILABLE_ON_MXM = "Error: lyrics not available on musixmatch 😥"; # some songs are restricted on Musixmatch
our $ERROR_INVALID_INPUT = "Error: invalid input";
multi sub MAIN($artist, $title) {
my $song_str = "{$artist.uc} - {$title.uc}";
say_getting_lyrics_for($song_str);
my ($kebab_a, $kebab_t) = kebab($artist), kebab($title);
my %resp = get_lyrics($kebab_a, $kebab_t);
if %resp<lyrics> {
write_lyrics_to_f(%resp<lyrics>, $artist, $title) unless %resp<error>; # fail silently and retry
} else {
my ($percent_a, $percent_t) = percent_encode($artist), percent_encode($title);
my $qs = "$percent_a%20$percent_t";
get_by_qs($qs);
}
}
multi sub MAIN($file) {
my @parsed = parse_text_file($file);
for @parsed {
say_getting_lyrics_for(percent_decode($_.uc));
get_by_qs($_)
}
}
# We don't overload this because the implementation is exactly the same
multi sub get_lyrics($artist = "", $title = "", $slug = "") {
my $url = $slug ?? "$BASEURL/$slug" !! "$BASEURL/lyrics/$artist/$title";
my %res = http_req_get_dom($url);
return %res if %res<error>;
my @lyrics = do for %res<dom>.find('.lyrics__content__ok') { $_.text if $_.text }
return %{error => Nil, lyrics => @lyrics}
}
sub get_search_results($qs) {
my $url = ("$BASEURL/search/$qs");
my %res = http_req_get_dom($url);
return %res if %res<error>;
my @matches = [];
for %res<dom>.find('a.title') -> $el {
@matches.push(%{title => $el.all-text, url => $el.attr<href>})
}
return %{error => Nil, matches => @matches}
}
#`(
Generic http get. It will either return:
1. Error, or
2. Representation of the DOM if request succeeded
Used by sub get_lyrics and sub get_search_results
)
sub http_req_get_dom($url) {
my $response = HTTP::Tiny.new.get($url);
return %{error => "Err: $response<reason>"} if not $response<status> == 200;
my $body = $response<content>.decode;
return %{error => Nil, dom => DOM::Tiny.parse($body)}
}
sub get_by_qs($qs) {
my @matches = get_matches($qs);
my $input = get;
exit if $input eq 0.Str;
try {
my $idx = $input.Int - 1;
if not $idx > @matches.elems and not $idx < 0 {
my %selection = @matches[$idx];
my ($a, $t) = %selection<url>.split("/")[*-2..*];
my $song_str = "{$a.uc} - {$t.uc}";
say_getting_lyrics_for($song_str);
my %resp = get_lyrics("", "", %selection<url>);
return say %resp<error> if %resp<error>;
if %resp<lyrics> {
write_lyrics_to_f(%resp<lyrics>, $a, $t)
} else {
say $LYRICS_NOT_AVAILABLE_ON_MXM;
}
}
}
if $! {
say $ERROR_INVALID_INPUT;
}
}
sub get_matches($qs) {
my %resp = get_search_results($qs);
return say %resp<error> if %resp<error>;
my (@matches) = %resp<matches>;
say_pretty_matches(@matches);
return @matches;
}
sub determine_fp($fname) {
my $fp;
if %*ENV<LRIX_DOWNLOAD_DIR> {
try {
mkdir %*ENV<LRIX_DOWNLOAD_DIR> unless %*ENV<LRIX_DOWNLOAD_DIR>.IO ~~ :d;
$fp = "%*ENV<LRIX_DOWNLOAD_DIR>/$fname";
}
if $! {
say $!;
}
} else {
my $basedir_exists = $BASEDIR.IO ~~ :d;
$BASEDIR.IO.mkdir unless $basedir_exists;
$fp = "$BASEDIR/$fname";
}
return $fp;
}
sub write_lyrics_to_f(@resp, $a, $t) {
my $fname = "$a - $t.txt";
my $fp = determine_fp($fname);
spurt($fp, ""); # clear file contents
for (@resp) -> $s {
say $s if $s;
try {
spurt($fp, $s, :append) unless not $s;
}
if $! {
die $!;
}
say "Wrote lyrics to $fp 🔥";
}
}
# musixmatch artist / track names are usually kebab
sub kebab($str) {
my $tc = map { .tc },($str.split(" "));
return $tc.join("-")
}
# for spaces
sub percent_encode($str) {
return $str.split(" ").join("%20");
}
sub percent_decode($str) {
return $str.split("%20").join(" ");
}
sub say_getting_lyrics_for($song_str) {
say "Getting lyrics for \"$song_str\" ⏳";
}
sub say_pretty_matches(@matches) {
say "Found results 🔍";
for @matches.kv -> $i, %el {
my ($title, $url) = %el<title>, %el<url>;
say "{$i+1}. $title: $url"
}
say "0. Exit";
}
sub parse_text_file($file) {
my $f = slurp($file).lines;
my @a = ($f.grep: { $^lines ne "" }).map: {
my $song_str = $^str.split("-").map: { $^str.trim }
percent_encode($song_str);
}
return @a;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment