All,
I had the challenge to import my M3U playlists into volumio. I couldn’t find anything that worked for me so I tried to create a bash script which would do the trick.
It does what it should do … but not very fast, but I don’t care as long as it does what it should do. See it as a first version. If you are not happy with it (and you have some programming skills) modify my script and enjoy!
As I said, it’s not very fast but it supports utf-8 but supports only simple M3U files, meaning the header, so it identifies itself as a M3U playlist, followed by full paths to the track.
I run it on my volumio on a Pi. I advise, at least for the first time, you run it with the -d option so you get debug info and feedback that it is doing something LOL.
Finally; what it doesn’t do is albumcover … I haven’t figured out how to do that. You have an idea! Update my code and upload it!
Here is the code, pretty well documented, copy it into a file (m3u2volumio) on your volumio instance, make it executable and have fun!
Feedback always appreciated!
Cheers,
Arjan
#!/bin/bash
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
MY_NAME=$(basename "${0}")
MYVERSION=1
#
# For the real devlopers .. Yes this script is not performant, can be written better BUT it does what it should do :)
#
# It has by default a debug() en debug-n() function to help debugging and be verbose. However it does
# hit a performance penaulty. If that's to much ..... then change every line holding
#
# "debug " into "#debug "
#
# and
#
# "debug-n " into "#debug-n "
#
# First how does a playlist within volumio (found in /data/playlists) look like.
# Every playlist is build out of a number of entries. An example entry is (in real life it is one line
# split it up for readability):
#
# {"service":"mpd",
# "uri":"mnt/NAS/Muziek/0-9/10CC/1973 - 10cc/03 - Donna.flac",
# "title":"Donna",
# "artist":"10CC",
# "album":"10cc",
# "albumart":"/albumart?cacheid=795&web=10CC/10cc/extralarge&path=%2Fmnt%2FNAS%2FMuziek%2F0-9%2F10CC%2F1973%20-%2010cc&icon=fa-tags&metadata=false"}
#
# A volumio Playlist will become
#
# [entry1, entry2, ...., entryN]
#
# Explanation how the script does it's trick
# ------------------------------------------
# The input playlist has a basedir path which can be different from what volumio expects. How ....
#
# In my setup I have a NAS (called nas542) where all (audio) files reside. Those files are controlled/edited by my PC.
# On my PC I have mounted the audio files on /Media/audio. Therefore all the playlist I create will have tracks starting with /Media/audio.
# The playlist I have created are actually stored somewhere under /Media/audio (to be precise /Media/audio/Playlist/arjan).
#
# Example of a track in one of my (M3U) playlists:
#
# /Media/audio/W/Who/1973 Quadrophenia/02.-The Real Me.flac
#
# In volumio I have configured that music can be found on a networkdisk \\nas542\audio and gave it the alias Muziek
# Volumio has mounted this \\nas542\audio on /mnt/NAS/Muziek (this is controlled by volumio based on how you configured it in the UI).
# Therefore the example track I showed above, is in the context of volumio:
#
# /mnt/NAS/Muziek/W/Who/1973 Quadrophenia/02.-The Real Me.flac
#
# After I created a playlist via volumio in volumo, I examed the created playlist (located in /data/playlist). It turns out that the path to the track
# in the volumo playlist is:
#
# mnt/NAS/Muziek/W/Who/1973 Quadrophenia/02.-The Real Me.flac
#
# It has removed the first /
#
# Therefore the track in the m3u playlist:
#
# /Media/audio/W/Who/1973 Quadrophenia/02.-The Real Me.flac
#
# becomes in the playlist on volumio:
#
# mnt/NAS/Muziek/W/Who/1973 Quadrophenia/02.-The Real Me.flac
#
# The script needs to make this translation
#
# Let's go!
#
#
# Directories and settings, all from the perspective of the system
# where you run this tool
#
# The location where the input M3U playlists kan be found
M3UTOPDIR=/mnt/NAS/Muziek/Playlists
# All tracks in the input M3U file start with the following base path
INPUTBASE=/Media/audio
# The paht of all tracks in the in the volumio playlist should start with this
PLAYLISTBASE=mnt/NAS/Muziek
# The location where all the M3U input playlist can be found.
# This can be a directory structure but it is assumed that all
# M3U playlist have NOT the same filename
VOLUMIOPLAYLISTDIR=/data/playlist
# Location of the albumart folder within volumio
ALBUMARTPATH=/data
#
#
#
# Changes below this ..... know what your are doing
#
#
#
#
# A temporary file
ALLM3U=/tmp/allm3u$$.txt
# File for error messages
ERROR=/tmp/error$$.txt
# It is now ...
NOW=$(date +%F:%T)
# Is the first line with a trick in the M3U file coming
FIRSTLINECOMING=false
# Do we want debug messages
DEBUG=false
# All trakcs in utf-8
TRACKSUTF8="/tmp/tracks-utf8-$$.txt"
touch "${ERROR}"
#
# Help functions
#
show_help() {
echo "This tool imports .m3u files into Volumio playlists"
echo " "
echo "Options:"
echo " -h Show help"
echo " -v Show version"
echo " -d Enable debug mode"
echo " -M Directory with M3U playlists (default: ${M3UTOPDIR})"
echo " -V Directory for Volumio playlists (default: ${VOLUMIOPLAYLISTDIR})"
echo " -I Base dir of input files (default: ${INPUTBASE})"
echo " -P Base dir for Volumio playlist entries (default: ${PLAYLISTBASE})"
}
#
# error message
#
show_error() {
echo "Unknown option or incorrect command line."
echo "Use -h to get help."
}
#
# debug funtions
#
debug() {
if ${DEBUG}; then
echo "${1}"
fi
}
debug-n() {
if ${DEBUG}; then
echo -n "${1}"
fi
}
#
# Escape JSON‑unsafe characters
#
json_escape() {
echo -n "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
#
# Convert metadata safely to UTF‑8
#
safe_utf8() {
iconv -f UTF-8 -t UTF-8//TRANSLIT 2>/dev/null <<< "$1"
}
#
# Write a playlist entry
#
write_entry() {
echo -n "{\"service\":\"mpd\",\"uri\":\"${URI}\"," >> "${OUTPUT}"
# Title
if [[ ${URI} == *.flac ]]; then
TITLE=$(metaflac --show-tag=title "/${URI}" 2>>"${ERROR}" | cut -d= -f2)
elif [[ ${URI} == *.mp3 ]]; then
TITLE=$(id3v2 --list "/${URI}" 2>>"${ERROR}" | grep ^TIT2 | sed 's/^TIT2 (.*): //')
fi
TITLE=$(safe_utf8 "${TITLE}")
TITLE=$(json_escape "${TITLE:-TBD}")
echo -n "\"title\":\"${TITLE}\"," >> "${OUTPUT}"
# Artist
if [[ ${URI} == *.flac ]]; then
ARTIST=$(metaflac --show-tag=artist "/${URI}" 2>>"${ERROR}" | cut -d= -f2)
elif [[ ${URI} == *.mp3 ]]; then
ARTIST=$(id3v2 --list "/${URI}" 2>>"${ERROR}" | grep ^TPE1 | sed 's/^TPE1 (.*): //')
fi
ARTIST=$(safe_utf8 "${ARTIST}")
ARTIST=$(json_escape "${ARTIST:-TBD}")
echo -n "\"artist\":\"${ARTIST}\"," >> "${OUTPUT}"
# Album
if [[ ${URI} == *.flac ]]; then
ALBUM=$(metaflac --show-tag=album "/${URI}" 2>>"${ERROR}" | cut -d= -f2)
elif [[ ${URI} == *.mp3 ]]; then
ALBUM=$(id3v2 --list "/${URI}" 2>>"${ERROR}" | grep ^TALB | sed 's/^TALB (.*): //')
fi
ALBUM=$(safe_utf8 "${ALBUM}")
ALBUM=$(json_escape "${ALBUM:-TBD}")
echo -n "\"album\":\"${ALBUM}\"," >> "${OUTPUT}"
# Albumcover placeholder
#
# I haven't figured out how to do this ....
#
echo -n "\"albumcover\":\"TBD\"}" >> "${OUTPUT}"
}
#
# Command‑line parsing
#
PARSED_ARGUMENTS=$(getopt -n m3u2volumio -o dvhM:V:I:P: -- "$@")
VALID_ARGUMENTS=$?
if [ "$VALID_ARGUMENTS" != "0" ]; then
{
show_error
exit 1
}
fi
eval set -- "$PARSED_ARGUMENTS"
while true
do
case "$1" in
-M) M3UTOPDIR="${2}"; shift 2 ;;
-V) VOLUMIOPLAYLISTDIR="${2}"; shift 2 ;;
-I) INPUTBASE="${2}"; shift 2 ;;
-P) PLAYLISTBASE="${2}"; shift 2 ;;
-d) DEBUG=true; shift ;;
-v) echo "Version: ${MYVERSION}"; exit 0 ;;
-h) show_help; exit 0 ;;
--) shift; break ;;
*) show_error; exit 3 ;;
esac
done
#
# Directory checks
#
for d in "${M3UTOPDIR}" "${VOLUMIOPLAYLISTDIR}"
do
if [ ! -d "${d}" ]; then
echo "${d} does not exist"
exit 4
fi
done
#
# find all playlist, they could be in a directory structure
# It is assumed that every m3u playlist has a unique filename
#
find "${M3UTOPDIR}" -type f -name "*.m3u" > "${ALLM3U}"
#
# Process each M3U file
#
while read -r M3U
do
debug "Processing file ${M3U} with appr. $(wc -l < "${M3U}") tracks"
# Convert to guaranteed UTF‑8
if ! iconv -f UTF-8 -t UTF-8 "${M3U}" > "${TRACKSUTF8}" 2>>"${ERROR}"; then
{
debug "Invalid UTF‑8 in ${M3U}, skipping"
echo "Invalid UTF‑8 in ${M3U}, skipping" >> "${ERROR}"
continue
}
fi
#
# Just started with a new M3U file, we expect the first line with
# a track is still coming.
# Once whe have that it becomes 'false'
FIRSTLINECOMING=true
while read -r LINE
do
debug-n "."
if [ "${LINE}" = "#EXTM3U" ]; then
{
# First line of a M3U, create a ne output file
OUTPUT="${VOLUMIOPLAYLISTDIR}/$(basename "${M3U}") | sed -e 's/.m3u//' "
#
# Does this output file already exist
#
if [ -f "${OUTPUT}" ]; then
{
debug " "
debug "Backup original ${OUTPUT}"
mv "${OUTPUT}" "${OUTPUT}-${NOW}"
}
fi
#
# Create the OUTPUT file and put the first char into it
#
echo -n '[' > "${OUTPUT}"
# Read next line
continue
}
fi
TRACK=$(echo "${LINE}" | sed -e "s#${INPUTBASE}#${PLAYLISTBASE}#")
if [ -f "/${TRACK}" ]; then
URI="${TRACK}"
if ${FIRSTLINECOMING}; then
FIRSTLINECOMING=false
else
echo -n ',' >> "${OUTPUT}"
fi
write_entry
else
{
debug " "
debug "Track does not exist therefore skipped: /${TRACK}"
echo "Track does not exist therefore skipped: /${TRACK} in ${M3U}" >> "${ERROR}"
}
fi
done < "${TRACKSUTF8}"
debug "Done"
echo -n ']' >> "${OUTPUT}"
rm -f "${TRACKSUTF8}"
done < "${ALLM3U}"
# Show errors if any
if [ "$(wc -l < "${ERROR}")" -ne 0 ]; then
echo "Found these errors:"
cat "${ERROR}"
fi
rm -f "${ALLM3U}" "${ERROR}"