Skip to content

Instantly share code, notes, and snippets.

@elundmark
Last active April 9, 2023 11:40
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save elundmark/a3c1cc9b438ff58fbed584e803431a9c to your computer and use it in GitHub Desktop.
Save elundmark/a3c1cc9b438ff58fbed584e803431a9c to your computer and use it in GitHub Desktop.
Make m3u playlist of a Youtube Playlist (youtube-dl & jq must be installed)
#!/usr/bin/env bash
# $ ./make_m3u_youtube_playlist.sh [-d OUTPUT-DIR] URL [URL ...]
if ! which jq &>/dev/null || ! which youtube-dl &>/dev/null; then
echo "'youtube-dl' and 'jq' must be installed in \$PATH, exiting." 1>&2
exit 1
fi
declare log_file="$HOME/.make_m3u_youtube_playlist.log"
declare prefix_id='https://www.youtube.com/watch?v='
declare prefix_pls='https://www.youtube.com/playlist?list='
declare pls_url
declare pls_re='^https?://www\.youtube\.com/(watch|playlist)\?.*?list=([^&]+)'
declare pls_n
declare pls_json
declare pls_u
declare pls_ids
declare pls_titles
declare -i replace_tmp=0
declare -a pls_i_arr=()
declare -a pls_t_arr=()
declare top_str='#EXTM3U'$'\n'
declare str
declare file_name
declare desired_filename
declare -i pls_len=0
declare output_dir="$PWD" # where to write the m3u file
declare json_re='\{.+\}'
declare -a args=()
function log_echo ()
{
echo "${*}" 1>&2
echo "${*}" >> "$log_file"
return $?
}
while getopts ":d:" options ; do
case "$options" in
d) output_dir="$(sed -r 's#/+$##' <<< "$OPTARG")";; # override directory
*) log_echo "Not a valid option, exiting:" "${*}"
exit 1;;
esac
done
args+=("${@:OPTIND}")
# enable support for multiple playlists at once
for i in "${!args[@]}" ; do
[[ ${args[i]} = '' ]] && continue
pls_url="${args[i]}"
# reset loop variables
replace_tmp=0
pls_len=0
file_name=''
str="$top_str"
pls_i_arr=()
pls_t_arr=()
if [[ ! $pls_url =~ $pls_re ]] ; then
log_echo "No valid playlist url given, exiting:" "$pls_url"
exit 1
fi
pls_url="${BASH_REMATCH[2]}"
pls_u="$(
# Get the playlist's owner / username - lazy way
curl -s -L -A "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0" "${prefix_pls}${pls_url}" \
| grep -oP 'channel-title.+?<a [^>]+>[^<]+</a>' \
| sed -r 's#.+>([^<]+)</a>#\1#'
)"
if [[ $pls_u = '' ]]; then
log_echo "No user found, exiting:" "$pls_u"
exit 1
fi
pls_json="$(youtube-dl --ignore-config --flat-playlist --dump-single-json "${prefix_pls}${pls_url}")"
if [[ $? -ne 0 || ! "$pls_json" =~ $json_re ]] ; then
log_echo "Not a playlist or youtube-dl returned an error, exiting:" "$pls_json"
exit 1
fi
# verify it's valid json first
if ! jq . <<< "$pls_json" &>/dev/null; then
log_echo "jq could not parse string, exiting:" "$pls_json"
exit 1
fi
pls_ids="$(jq -rM '.entries[].id' <<< "$pls_json")" || exit 3
pls_titles="$(jq -rM '.entries[].title' <<< "$pls_json")" || exit 4
pls_n="$(jq -rM '.title' <<< "$pls_json")" || exit 5
if [[ $pls_n = '' ]] ; then
# Allow empty name
pls_n="Untitled"
fi
pls_len=$(wc -l <<< "$pls_ids")
if [[ $pls_len -eq 0 ]] ; then
# Don't write empty playlists
log_echo "No videos in playlist, exiting:" "$pls_len"
exit 1
fi
# Turn lines into arrays
while read -r LINE; do
[[ $LINE = '' ]] && continue
# Full url
pls_i_arr+=("${prefix_id}${LINE}")
done <<< "$pls_ids"
while read -r LINE; do
[[ $LINE = '' ]] && continue
pls_t_arr+=("$LINE")
done <<< "$pls_titles"
# Build m3u string
# https://en.wikipedia.org/wiki/M3U
# Playlist will be missing the length property but it should work (does in mpv)
for x in "${!pls_i_arr[@]}" ; do
[[ ${pls_i_arr[x]} = '' ]] && continue
# #EXTM3U
# #EXTINF:,User - Title - YT-PL-ID i/size
# https://www.youtube.com/watch?v=AAAAAAAAAAA
#
str="${str}#EXTINF:,$pls_u - $pls_n - ${pls_t_arr[x]} - $pls_url $((x+1))/$pls_len"$'\n'"${pls_i_arr[x]}"$'\n'$'\n'
done
desired_filename="$output_dir/$pls_u - $pls_n ($pls_len).m3u"
file_name="$desired_filename"
if [[ -e $file_name ]] ; then
log_echo "creating tmp file and backing up existing file:" "$file_name"
file_name="$(mktemp -p "$output_dir")"
replace_tmp=1
fi
echo -n "$str" > "$file_name"
if [[ $replace_tmp -eq 1 ]] ; then
# depend on mv's backup functionallity to create a bak file
mv -v --backup=t "$file_name" "$desired_filename" | tee -a "$log_file" 1>&2
file_name="$desired_filename"
fi
if [[ $? -ne 0 || ! -f "$file_name" ]] ; then
log_echo "$file_name"
notify-send -i 'error' "M3U playlist" "Failed to create playlist"
else
echo "$file_name"
notify-send -i 'gtk-ok' "M3U playlist created" "$(basename "$file_name" .m3u)"
fi
done
exit $?
@andreicaba
Copy link

why does it ask for the user?!...

@icha1
Copy link

icha1 commented Mar 19, 2021

I get
./make_m3u_youtube_playlist.sh: line 66: unexpected EOF while looking for matching )' ./make_m3u_youtube_playlist.sh: line 143: syntax error: unexpected end of file`

@charsi
Copy link

charsi commented May 16, 2021

This worked for me - https://superuser.com/a/1359132

youtube-dl -j --flat-playlist <PLAYLIST_LINK> | jq -r '.id' | sed 's_^_https://youtu.be/_' > playlist.m3u

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment