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