dotfiles

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

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