dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

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