···56# LyricFetch
78-Fetches lyrics from Genius and adds them to your music library's metadata.
910-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.
1617# ADB Auto Connect
18Automatically find and connect to an Android device with Wireless Debugging over the local network.
···56# LyricFetch
78+Fetches lyrics from Genius using a [Dumb](https://github.com/rramiachraf/dumb) instance, and adds them to your music library's metadata.
910+Currently only supports FLAC files. In the future, timesynced lyrics may be supported.
000001112# ADB Auto Connect
13Automatically find and connect to an Android device with Wireless Debugging over the local network.
+132-71
scripts/lyricfetch.nu
···1#!/usr/bin/nu
2003def main [
4- --noconfirm (-y)
000005] {
6- const library = "~/Music" | path expand
7- print $"Library path set to (ansi blue)($library)(ansi reset)"
0089- 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- }
2122- 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."
2526- for track in $tracks {
27- let meta = metaflac $track --show-all-tags
28 | parse "{key}={value}"
29 | reduce --fold {} {|it, acc| $acc | upsert ($it.key | str downcase) $it.value}
3031 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 }
3536- print $"\n<- (ansi blue)($meta.title)(ansi reset) by (ansi purple)($meta.artist)(ansi reset) ->"
0003738- 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- }
0000000000000000000000005253- 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- }
00000000000000000000006061- 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- }
7576- 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- }
000000089}
9000000000000
···1#!/usr/bin/nu
23+use std [log null-device]
4+5def main [
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
12] {
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"
1718+ 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+ }
000002526+ $files | each {|file|
27+ if $minimal_screen {print (ansi clear_entire_screen) ; print (ansi cursor_home)}
02829+ let meta = metaflac $file --show-all-tags
030 | parse "{key}={value}"
31 | reduce --fold {} {|it, acc| $acc | upsert ($it.key | str downcase) $it.value}
3233 if ($meta.artist? == null) or ($meta.title? == null) {
34+ print $"(ansi yellow)`($file)`(ansi red) doesn't contain artist and title tags, cannot search for lyrics(ansi reset)"
35+ return
36 }
3738+ if ($meta.lyrics? | is-not-empty) and not $update {
39+ log debug $"($meta.artist) – ($meta.title) by already contains lyrics, skipping"
40+ return
41+ }
4243+ 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+}
8182+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+ }
111112+ 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+}
00000000118119+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
139}
140141+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+}