···5566# LyricFetch
7788-Fetches lyrics from Genius and adds them to your music library's metadata.
88+Fetches lyrics from Genius using a [Dumb](https://github.com/rramiachraf/dumb) instance, and adds them to your music library's metadata.
991010-Currently only supports FLAC files. In the future, timesynced lyrics may be
1111-supported, and auto-fetching may be more reliable.
1212-1313-I don't recommend using this right now. It's pretty bad at searching, and most
1414-of the time can't auto-fetch the lyrics anyway. Personally, I just fetch the lyrics
1515-manually and save them using Recordbox.
1010+Currently only supports FLAC files. In the future, timesynced lyrics may be supported.
16111712# ADB Auto Connect
1813Automatically find and connect to an Android device with Wireless Debugging over the local network.
+132-71
scripts/lyricfetch.nu
···11#!/usr/bin/nu
2233+use std [log null-device]
44+35def main [
44- --noconfirm (-y)
66+ --library (-l): path = ("~/Music" | path expand)
77+ --dumb-instance (-d): string = "https://dumb.hyperreal.coffee"
88+ --providers (-p): list = [dumb]
99+ --update (-u) # Overwrite existing lyrics
1010+ --minimal_screen (-m) # Clear the screen before each track
1111+ --clear # Clears all existing lyrics from your library
512] {
66- const library = "~/Music" | path expand
77- print $"Library path set to (ansi blue)($library)(ansi reset)"
1313+ if $minimal_screen {print (ansi clear_entire_screen) ; print (ansi cursor_home)}
1414+ print "Indexing compatible files in library"
1515+ let files = fd -g '*.flac' $library | lines
1616+ print $"(ansi blue)($files | length)(ansi reset) compatible files found"
81799- if (which metaflac | is-empty) {
1010- error make {
1111- msg: "Could not find metaflac."
1212- help: "Install the FLAC utils."
1313- }
1414- }
1515- if (which fd | is-empty) {
1616- error make {
1717- msg: "Could not find fd"
1818- help: "Install fd-find."
1919- }
2020- }
1818+ if $clear {
1919+ print "Clearing all lyrics from library"
2020+ $files | par-each {|file|
2121+ metaflac $file --remove-tag "LYRICS"
2222+ }
2323+ return null
2424+ }
21252222- print "Indexing Library..."
2323- let tracks = fd -g '*.flac' $library | lines
2424- print $"Found (ansi green)($tracks | length)(ansi reset) supported tracks in this library."
2626+ $files | each {|file|
2727+ if $minimal_screen {print (ansi clear_entire_screen) ; print (ansi cursor_home)}
25282626- for track in $tracks {
2727- let meta = metaflac $track --show-all-tags
2929+ let meta = metaflac $file --show-all-tags
2830 | parse "{key}={value}"
2931 | reduce --fold {} {|it, acc| $acc | upsert ($it.key | str downcase) $it.value}
30323133 if ($meta.artist? == null) or ($meta.title? == null) {
3232- print $"(ansi red)($track)(ansi yellow) doesn't contain artist and title tags, cannot search for lyrics. Skipping.(ansi reset)"
3333- continue
3434+ print $"(ansi yellow)`($file)`(ansi red) doesn't contain artist and title tags, cannot search for lyrics(ansi reset)"
3535+ return
3436 }
35373636- print $"\n<- (ansi blue)($meta.title)(ansi reset) by (ansi purple)($meta.artist)(ansi reset) ->"
3838+ if ($meta.lyrics? | is-not-empty) and not $update {
3939+ log debug $"($meta.artist) – ($meta.title) by already contains lyrics, skipping"
4040+ return
4141+ }
37423838- if ($meta.lyrics? | is-not-empty) {
3939- print $"(ansi green)Track already contains lyrics. Skipping.(ansi reset)"
4040- continue
4141- } else if ($meta.unsyncedlyrics? | is-not-empty) {
4242- print -n $"Track already contains unsynced lyrics. Would you like to copy them to the lyrics tag? [Y/n]: "
4343- match (input) {
4444- "n" | "N" => (print "Not copying unsynced lyrics, continuing.")
4545- _ => {
4646- print "Copying unsynced lyrics to lyrics tag..."
4747- metaflac $track --set-tag $"LYRICS=($meta.unsyncedlyrics)"
4848- continue
4949- }
5050- }
5151- }
4343+ if not $minimal_screen {print}
4444+ print $"<- Fetching lyrics for (ansi green)($meta.artist) – ($meta.title)(ansi reset) ->"
4545+ fetch-lyrics dumb --instance $dumb_instance $"($meta.artist) ($meta.title)"
4646+ | if $in == null {
4747+ print $"(ansi red)No lyrics found for (ansi yellow)($meta.artist) – ($meta.title)(ansi reset)"
4848+ } else {
4949+ metaflac $file --set-tag $"LYRICS=($in)"
5050+ print $"Updated lyrics for (ansi green)($meta.artist) – ($meta.title)(ansi reset)"
5151+ }
5252+ }
5353+5454+ return null
5555+}
5656+5757+def "fetch-lyrics dumb" [
5858+ --instance: string
5959+ search: string
6060+] {
6161+ if $instance == null {
6262+ error make {
6363+ msg: "Please provide a Dumb instance URL with --instance. For example: `--instance='https://dumb.hyperreal.coffee'`"
6464+ }
6565+ }
6666+6767+ log debug $"Searching for: `($search)`"
6868+ parse-webpage dumb search $"($instance)/search?q=($search | url encode)"
6969+ | if $in == null {
7070+ return null
7171+ } else {
7272+ print -n $"Best result: (ansi green)($in.title)(ansi reset) by (ansi green)($in.artist)(ansi reset). Is this correct? [Y/n]: "
7373+ match (input -n 1 | str downcase) {
7474+ "y" | "" => null
7575+ "n" | _ => (return null)
7676+ }
7777+ log debug $"Parsing lyrics from webpage: `($instance)($in.href)`"
7878+ parse-webpage dumb lyrics $"($instance)($in.href)"
7979+ }
8080+}
52815353- print $"Fetching lyrics..."
5454- let info = try {
5555- http get $"https://lyrics.astrid.sh/api/search?q=($meta.artist) ($meta.title)"
5656- } catch {
5757- print $"(ansi yellow)Could not find lyrics, skipping.(ansi reset)"
5858- continue
5959- }
8282+def "parse-webpage dumb search" [
8383+ url: string
8484+] {
8585+ let item = http get $url
8686+ | xmllint --html --xmlout --nonet - e> (null-device)
8787+ | lines
8888+ | last 1
8989+ | str join "\n"
9090+ | from xml
9191+ | get content
9292+ | where tag == body
9393+ | get 0.content
9494+ | where tag == main
9595+ | get 0.content
9696+ | where attributes.id? == search-page
9797+ | get 0.content
9898+ | where attributes.id? == search-results
9999+ | get 0.content
100100+ | where content.0.content.0.content? == Songs
101101+ | try {
102102+ get 0.content.1
103103+ } catch {|err|
104104+ if $err.debug starts-with "AccessBeyondEnd" {
105105+ log debug "No results for search."
106106+ return null
107107+ } else {
108108+ $err.raw
109109+ }
110110+ }
601116161- let lyrics = if ($info.lyrics | is-empty) {
6262- print $"(ansi yellow)Could not automatically fetch lyrics.(ansi reset)"
6363- print -n $"Please manually fetch the lyrics from (ansi default_dimmed)($info.url | ansi link)(ansi reset), then paste them here \(enter to skip\):"
6464- let input = input
6565- if ($input | is-empty) {
6666- print $"(ansi yellow)Manually skipped.(ansi reset)"
6767- continue
6868- } else {
6969- $input
7070- }
7171- } else {
7272- print "Successfully fetched lyrics."
7373- $info.lyrics
7474- }
112112+ return {
113113+ artist: $item.content.1.content.0.content.0.content
114114+ title: $item.content.1.content.1.content.0.content
115115+ href: $item.attributes.href
116116+ }
117117+}
751187676- print $"(ansi green_dimmed)<========>(ansi reset)\n(ansi green)($lyrics)(ansi reset)\n(ansi green_dimmed)<========>(ansi reset)"
7777- print -n $"\nAre the above lyrics correct? [Y/n]: "
7878- match (input) {
7979- "n" | "N" => {
8080- print $"(ansi yellow)Manually skipped.(ansi reset)"
8181- continue
8282- }
8383- _ => {
8484- print "Writing lyrics to track metadata..."
8585- metaflac $track --set-tag $"LYRICS=($lyrics)"
8686- }
8787- }
8888- }
119119+def "parse-webpage dumb lyrics" [
120120+ url: string
121121+] {
122122+ http get $url
123123+ | xmllint --html --xmlout --nonet - e> (null-device)
124124+ | lines
125125+ | last 1
126126+ | str join "\n"
127127+ | from xml
128128+ | get content
129129+ | where tag == body
130130+ | get 0.content
131131+ | where tag == main
132132+ | get 0.content
133133+ | where attributes.id? == container
134134+ | get 0.content
135135+ | where attributes.id? == lyrics
136136+ | get 0.content
137137+ | where tag == null or tag == br or tag == a
138138+ | parse-fragments dumb lyrics
89139}
90140141141+def "parse-fragments dumb lyrics" []: any -> any {
142142+ par-each -k {|e|
143143+ match $e.tag {
144144+ null => {$e.content}
145145+ "br" => {"\n"}
146146+ "a" => {$e.content.0.content | parse-fragments dumb lyrics}
147147+ _ => (log warning $"Unexpected tag in parse-fragments: `($e.tag)`")
148148+ }
149149+ }
150150+ | str join
151151+}