Various scripts that I maintain

Replace LyricFetch backend with Dumb

Signed-off-by: Shiloh Fen <shiloh@shilohfen.com>

+134 -78
+2 -7
README.md
··· 5 5 6 6 # LyricFetch 7 7 8 - Fetches lyrics from Genius and adds them to your music library's metadata. 8 + Fetches lyrics from Genius using a [Dumb](https://github.com/rramiachraf/dumb) instance, and adds them to your music library's metadata. 9 9 10 - Currently only supports FLAC files. In the future, timesynced lyrics may be 11 - supported, and auto-fetching may be more reliable. 12 - 13 - I don't recommend using this right now. It's pretty bad at searching, and most 14 - of the time can't auto-fetch the lyrics anyway. Personally, I just fetch the lyrics 15 - manually and save them using Recordbox. 10 + Currently only supports FLAC files. In the future, timesynced lyrics may be supported. 16 11 17 12 # ADB Auto Connect 18 13 Automatically find and connect to an Android device with Wireless Debugging over the local network.
+132 -71
scripts/lyricfetch.nu
··· 1 1 #!/usr/bin/nu 2 2 3 + use std [log null-device] 4 + 3 5 def main [ 4 - --noconfirm (-y) 6 + --library (-l): path = ("~/Music" | path expand) 7 + --dumb-instance (-d): string = "https://dumb.hyperreal.coffee" 8 + --providers (-p): list = [dumb] 9 + --update (-u) # Overwrite existing lyrics 10 + --minimal_screen (-m) # Clear the screen before each track 11 + --clear # Clears all existing lyrics from your library 5 12 ] { 6 - const library = "~/Music" | path expand 7 - print $"Library path set to (ansi blue)($library)(ansi reset)" 13 + if $minimal_screen {print (ansi clear_entire_screen) ; print (ansi cursor_home)} 14 + print "Indexing compatible files in library" 15 + let files = fd -g '*.flac' $library | lines 16 + print $"(ansi blue)($files | length)(ansi reset) compatible files found" 8 17 9 - if (which metaflac | is-empty) { 10 - error make { 11 - msg: "Could not find metaflac." 12 - help: "Install the FLAC utils." 13 - } 14 - } 15 - if (which fd | is-empty) { 16 - error make { 17 - msg: "Could not find fd" 18 - help: "Install fd-find." 19 - } 20 - } 18 + if $clear { 19 + print "Clearing all lyrics from library" 20 + $files | par-each {|file| 21 + metaflac $file --remove-tag "LYRICS" 22 + } 23 + return null 24 + } 21 25 22 - print "Indexing Library..." 23 - let tracks = fd -g '*.flac' $library | lines 24 - print $"Found (ansi green)($tracks | length)(ansi reset) supported tracks in this library." 26 + $files | each {|file| 27 + if $minimal_screen {print (ansi clear_entire_screen) ; print (ansi cursor_home)} 25 28 26 - for track in $tracks { 27 - let meta = metaflac $track --show-all-tags 29 + let meta = metaflac $file --show-all-tags 28 30 | parse "{key}={value}" 29 31 | reduce --fold {} {|it, acc| $acc | upsert ($it.key | str downcase) $it.value} 30 32 31 33 if ($meta.artist? == null) or ($meta.title? == null) { 32 - print $"(ansi red)($track)(ansi yellow) doesn't contain artist and title tags, cannot search for lyrics. Skipping.(ansi reset)" 33 - continue 34 + print $"(ansi yellow)`($file)`(ansi red) doesn't contain artist and title tags, cannot search for lyrics(ansi reset)" 35 + return 34 36 } 35 37 36 - print $"\n<- (ansi blue)($meta.title)(ansi reset) by (ansi purple)($meta.artist)(ansi reset) ->" 38 + if ($meta.lyrics? | is-not-empty) and not $update { 39 + log debug $"($meta.artist) – ($meta.title) by already contains lyrics, skipping" 40 + return 41 + } 37 42 38 - if ($meta.lyrics? | is-not-empty) { 39 - print $"(ansi green)Track already contains lyrics. Skipping.(ansi reset)" 40 - continue 41 - } else if ($meta.unsyncedlyrics? | is-not-empty) { 42 - print -n $"Track already contains unsynced lyrics. Would you like to copy them to the lyrics tag? [Y/n]: " 43 - match (input) { 44 - "n" | "N" => (print "Not copying unsynced lyrics, continuing.") 45 - _ => { 46 - print "Copying unsynced lyrics to lyrics tag..." 47 - metaflac $track --set-tag $"LYRICS=($meta.unsyncedlyrics)" 48 - continue 49 - } 50 - } 51 - } 43 + if not $minimal_screen {print} 44 + print $"<- Fetching lyrics for (ansi green)($meta.artist) – ($meta.title)(ansi reset) ->" 45 + fetch-lyrics dumb --instance $dumb_instance $"($meta.artist) ($meta.title)" 46 + | if $in == null { 47 + print $"(ansi red)No lyrics found for (ansi yellow)($meta.artist) – ($meta.title)(ansi reset)" 48 + } else { 49 + metaflac $file --set-tag $"LYRICS=($in)" 50 + print $"Updated lyrics for (ansi green)($meta.artist) – ($meta.title)(ansi reset)" 51 + } 52 + } 53 + 54 + return null 55 + } 56 + 57 + def "fetch-lyrics dumb" [ 58 + --instance: string 59 + search: string 60 + ] { 61 + if $instance == null { 62 + error make { 63 + msg: "Please provide a Dumb instance URL with --instance. For example: `--instance='https://dumb.hyperreal.coffee'`" 64 + } 65 + } 66 + 67 + log debug $"Searching for: `($search)`" 68 + parse-webpage dumb search $"($instance)/search?q=($search | url encode)" 69 + | if $in == null { 70 + return null 71 + } else { 72 + print -n $"Best result: (ansi green)($in.title)(ansi reset) by (ansi green)($in.artist)(ansi reset). Is this correct? [Y/n]: " 73 + match (input -n 1 | str downcase) { 74 + "y" | "" => null 75 + "n" | _ => (return null) 76 + } 77 + log debug $"Parsing lyrics from webpage: `($instance)($in.href)`" 78 + parse-webpage dumb lyrics $"($instance)($in.href)" 79 + } 80 + } 52 81 53 - print $"Fetching lyrics..." 54 - let info = try { 55 - http get $"https://lyrics.astrid.sh/api/search?q=($meta.artist) ($meta.title)" 56 - } catch { 57 - print $"(ansi yellow)Could not find lyrics, skipping.(ansi reset)" 58 - continue 59 - } 82 + def "parse-webpage dumb search" [ 83 + url: string 84 + ] { 85 + let item = http get $url 86 + | xmllint --html --xmlout --nonet - e> (null-device) 87 + | lines 88 + | last 1 89 + | str join "\n" 90 + | from xml 91 + | get content 92 + | where tag == body 93 + | get 0.content 94 + | where tag == main 95 + | get 0.content 96 + | where attributes.id? == search-page 97 + | get 0.content 98 + | where attributes.id? == search-results 99 + | get 0.content 100 + | where content.0.content.0.content? == Songs 101 + | try { 102 + get 0.content.1 103 + } catch {|err| 104 + if $err.debug starts-with "AccessBeyondEnd" { 105 + log debug "No results for search." 106 + return null 107 + } else { 108 + $err.raw 109 + } 110 + } 60 111 61 - let lyrics = if ($info.lyrics | is-empty) { 62 - print $"(ansi yellow)Could not automatically fetch lyrics.(ansi reset)" 63 - print -n $"Please manually fetch the lyrics from (ansi default_dimmed)($info.url | ansi link)(ansi reset), then paste them here \(enter to skip\):" 64 - let input = input 65 - if ($input | is-empty) { 66 - print $"(ansi yellow)Manually skipped.(ansi reset)" 67 - continue 68 - } else { 69 - $input 70 - } 71 - } else { 72 - print "Successfully fetched lyrics." 73 - $info.lyrics 74 - } 112 + return { 113 + artist: $item.content.1.content.0.content.0.content 114 + title: $item.content.1.content.1.content.0.content 115 + href: $item.attributes.href 116 + } 117 + } 75 118 76 - print $"(ansi green_dimmed)<========>(ansi reset)\n(ansi green)($lyrics)(ansi reset)\n(ansi green_dimmed)<========>(ansi reset)" 77 - print -n $"\nAre the above lyrics correct? [Y/n]: " 78 - match (input) { 79 - "n" | "N" => { 80 - print $"(ansi yellow)Manually skipped.(ansi reset)" 81 - continue 82 - } 83 - _ => { 84 - print "Writing lyrics to track metadata..." 85 - metaflac $track --set-tag $"LYRICS=($lyrics)" 86 - } 87 - } 88 - } 119 + def "parse-webpage dumb lyrics" [ 120 + url: string 121 + ] { 122 + http get $url 123 + | xmllint --html --xmlout --nonet - e> (null-device) 124 + | lines 125 + | last 1 126 + | str join "\n" 127 + | from xml 128 + | get content 129 + | where tag == body 130 + | get 0.content 131 + | where tag == main 132 + | get 0.content 133 + | where attributes.id? == container 134 + | get 0.content 135 + | where attributes.id? == lyrics 136 + | get 0.content 137 + | where tag == null or tag == br or tag == a 138 + | parse-fragments dumb lyrics 89 139 } 90 140 141 + def "parse-fragments dumb lyrics" []: any -> any { 142 + par-each -k {|e| 143 + match $e.tag { 144 + null => {$e.content} 145 + "br" => {"\n"} 146 + "a" => {$e.content.0.content | parse-fragments dumb lyrics} 147 + _ => (log warning $"Unexpected tag in parse-fragments: `($e.tag)`") 148 + } 149 + } 150 + | str join 151 + }