mlm (7671B)
1 #!/bin/sh 2 # General music library management tasks. NOT multi-level marketing. 3 die() { printf '%s\n' "$1" >&2 && exit 1; } 4 checkdeps() { 5 for com in "$@"; do 6 command -v "$com" >/dev/null 2>&1 || 7 { printf '%s required but not found.\n' "$com" >&2 && exit 1; } 8 done 9 } 10 checkdeps rsync mpc eyeD3 ffprobe 11 12 # Check necessary variables 13 [ -z "$XDG_DATA_HOME" ] && die 'XDG_DATA_HOME not set.' 14 MPD_LOGFILE="$XDG_DATA_HOME"/mpd/mpd.log 15 [ -f "$MPD_LOGFILE" ] || die "$MPD_LOGFILE does not exist or is not a readable file." 16 [ -z "$MUSIC_DIR" ] && die 'MUSIC_DIR not set.' 17 [ -d "$MUSIC_DIR" ] || die "$MUSIC_DIR does not exist or is not a readable directory." 18 19 import() { 20 if [ -n "$(find . -mindepth 1 -maxdepth 1 -name '*.mp3')" ]; then 21 printf "MP3 files found in current directory!\nAre you sure you want to import? [Y/n] " 22 stty raw 23 yn="$(dd bs=1 count=1 2>/dev/null)" 24 stty -raw 25 case "$yn" in 26 Y* | y*) ;; 27 *) die 'User cancelled.' ;; 28 esac 29 fi 30 31 printf "Did you already run mp3gain? [Y/n] " 32 stty raw 33 yn="$(dd bs=1 count=1 2>/dev/null)" 34 stty -raw 35 printf '\n' 36 case "$yn" in 37 Y* | y*) ;; 38 *) die "Run it now then. 39 albums: parallel mp3gain -s i -a "{}/*.mp3' ::: */* 40 tracks: parallel mp3gain -s i -r '{}/*.mp3' ::: */' ;; 41 esac 42 43 # Sync current dir into music dir, (u)pdated only, (r)ecursively, (p)reserve 44 # permissions, info about whole transfer, delete originals 45 printf "Moving files into library...\n" 46 rsync -urp --info=progress2 --remove-source-files ./ "$MUSIC_DIR"/ 47 48 # Update the database, waiting for it to finish 49 printf "Updating MPD database...\n" 50 mpc -w update >/dev/null 2>&1 51 52 # Update the recently added playlist: 53 printf "Updating recently added playlist...\n" 54 55 # Create a tempdir for the files 56 tempdir="$(mktemp -d)" 57 trap 'rm -r "$tempdir"' INT TERM EXIT 58 59 # MPD logs all additions to the library, so extract from that. 60 # Sort for use in comm (1) 61 tac ~/.local/share/mpd/mpd.log | 62 awk -F 'added ' '/update: added/ { print $2 }' | 63 sort >"$tempdir"/sorted_newly_added.log 64 65 # Get the current contents of the recently added playlist, without M3U metadata 66 # Sort for use in comm (1) 67 grep -v '^#' "$MUSIC_DIR"/recently-added.m3u >"$tempdir"/current_recently_added.m3u 68 sort "$tempdir"/current_recently_added.m3u >"$tempdir"/sorted_recently_added.m3u 69 70 # Find the lines that have been logged by MPD as added but are not yet in the recently added playlist 71 # via comm (1), excluding lines present only in file 2 (MPD logfile) and in both. 72 # Then, add an M3U header, and concatenate with the current recently added playlist. 73 # The result is: [M3U header, newly added tracks, rest of current recently added playlist] 74 cat - "$tempdir"/current_recently_added.m3u \ 75 >"$MUSIC_DIR"/recently-added.m3u \ 76 <<PLAYLIST_END 77 #EXTM3U 78 #PLAYLIST:Recently Added 79 $(comm -23 "$tempdir"/sorted_newly_added.log "$tempdir"/sorted_recently_added.m3u) 80 PLAYLIST_END 81 82 # Untrap 83 trap - INT TERM EXIT 84 85 printf "Done!\n" 86 } 87 88 verify_playlist_rust() { 89 playlist_path="$MUSIC_DIR/$1" 90 [ -f "$playlist_path" ] || die "Playlist $playlist_path not found." 91 92 tmpbin=$(mktemp) 93 trap 'rm $tmpbin' INT TERM EXIT 94 { 95 rustc - -o "$tmpbin" <<EOF 96 use std::path::PathBuf; 97 use std::fs::File; 98 use std::io::{BufReader, BufRead}; 99 fn main() { 100 let music_dir = match std::env::var("MUSIC_DIR") { 101 Ok(v) => PathBuf::from(v), 102 _ => panic!("Variable MUSIC_DIR not set in environment."), 103 }; 104 assert!(music_dir.as_path().is_dir()); 105 106 let args = std::env::args(); 107 let m3u_file_name = args.skip(1).next().expect("No file provided"); 108 let m3u_file = File::open(m3u_file_name).expect("Couldn't read file"); 109 let reader = BufReader::new(m3u_file); 110 let mut linenum = 1; 111 for line in reader.lines() { 112 let line = line.expect("Couldn't read line"); 113 if !line.starts_with('#') { 114 let fpath = music_dir.join(line); 115 if !fpath.is_file() { 116 eprintln!("{}\t{}", linenum, fpath.display()); 117 } 118 } 119 linenum += 1; 120 } 121 } 122 EOF 123 } && "$tmpbin" "$playlist_path" 124 rm "$tmpbin" 125 trap - INT TERM EXIT 126 } 127 128 verify_playlist() { 129 printf 'Install rustc for faster speeds.\n' 130 playlist_path="$MUSIC_DIR/$1" 131 [ -f "$playlist_path" ] || die "Playlist $playlist_path not found." 132 133 lns="$(wc -l <"$playlist_path" | tr -d '[:space:]')" 134 ctr=1 135 while read -r f; do 136 f="$(printf '%s' "$f" | tr -d '\r\n')" 137 printf '%s' "$f" | grep '^#' >/dev/null 2>&1 && { 138 ctr=$((ctr + 1)) 139 continue 140 } 141 printf "\r%d/%d" $ctr "$lns" 142 stat "$MUSIC_DIR/$f" >/dev/null 2>&1 || printf "\t%s\n" "$f" 143 ctr=$((ctr + 1)) 144 done <"$playlist_path" 145 } 146 147 embed_lyrics() { 148 LYRICS_DIR="$HOME/.local/share/lyrics" 149 [ -d "$LYRICS_DIR" ] || die "Lyrics dir $LYRICS_DIR not found." 150 find "$LYRICS_DIR" -type f -size 0 -delete 151 152 find "$LYRICS_DIR" -type f | 153 while read -r lyricsfile; do 154 printf "Processing: %s\n" "$lyricsfile" && 155 fname="$(printf "%s" "${lyricsfile##*/}" | awk -F " - " '{gsub(".txt", "", $2); print "albumartist " "\"" $1 "\"" " title " "\"" $2 "\"" }' | xargs mpc find | head -n 1)" && 156 if [ -n "$fname" ]; then 157 eyeD3 --to-v2.4 --remove-all-lyrics --add-lyrics "$lyricsfile" "$MUSIC_DIR/$fname" && 158 rm "$lyricsfile" 159 else 160 rm "$lyricsfile" 161 fi 162 done 163 } 164 165 # args: source_dir, dest_dir 166 # Places all MP3 files in a directory into folders based on the artist and album. 167 # Removes any problematic characters. 168 # If you want to make changes in the current dir, run `mp3-tags-to-folders . .` 169 organize_files() { 170 [ $# -eq 2 ] || die "Arguments required: path to music directory, path to destination" 171 [ -d "$1" ] || die "Path $1 is not a directory." 172 src="$(realpath "$1")" 173 dst="$(realpath "$2")" 174 cd "$dst" || die "Could not cd to $dst" 175 mkdir -p _failed 176 find "$src" -name '*.mp3' 2>/dev/null | 177 while read -r i; do 178 artist="$(ffprobe -loglevel error -show_entries format_tags=album_artist -of default=noprint_wrappers=1:nokey=1 "$i" | tr -d ':' | tr '/+!@#$%^&*:?"|\\>' '_' | sed 's/\.\.*$//' | perl -Mopen=locale -Mutf8 -pe 's/[\x{0300}-\x{036F}]//g')" 179 [ -n "$artist" ] || { mv "$i" _failed/ && continue; } 180 album="$(ffprobe -loglevel error -show_entries format_tags=album -of default=noprint_wrappers=1:nokey=1 "$i" | tr -d ':' | tr '+!@#$%^&*:?>|\\/"' '_' | sed 's/\.\.*$//' | perl -Mopen=locale -Mutf8 -pe 's/[\x{0300}-\x{036F}]//g')" 181 [ -n "$album" ] || { mv "$i" _failed/ && continue; } 182 fname="$(printf '%s' "${i##*/}" | perl -Mopen=locale -Mutf8 -pe 's/[\x{0300}-\x{036F}]//g')" 183 mkdir -p "$artist/$album" 184 printf "%s -> %s\n" "$i" "$artist/$album/$fname" 185 mv "$i" "$artist/$album/$fname" 186 done 187 rmdir _failed 1>/dev/null 2>&1 || printf "Some tracks were not organised automatically, they are in ./_failed/" 188 } 189 190 # Show current song info & albumart 191 current() { 192 tmp="$(mktemp)" 193 trap 'rm $tmp' INT TERM EXIT 194 mpc readpicture "$(mpc current -f %file%)" >"$tmp" && mpv --keep-open=always --ontop "$tmp" 195 rm "$tmp" 196 trap - INT TERM EXIT 197 } 198 199 case "$1" in 200 import) import ;; 201 check-recent) 202 if command -v rustc >/dev/null 2>&1; then 203 verify_playlist_rust 'recently-added.m3u' 204 else 205 verify_playlist 'recently-added.m3u' 206 fi 207 ;; 208 check-playlist) 209 if command -v rustc >/dev/null 2>&1; then 210 verify_playlist_rust "$2" 211 else 212 verify_playlist "$2" 213 fi 214 ;; 215 embed-lyrics) embed_lyrics ;; 216 organise | organize) organize_files "$2" "$3" ;; 217 *) 218 printf "Supported commands: import, check-recent, check-playlist, embed-lyrics, organise\n" 219 exit 0 220 ;; 221 esac