330 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			330 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/bin/sh
 | ||
| 
 | ||
| # Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
 | ||
| # SPDX-License-Identifier: AGPL-3.0-or-later
 | ||
| #
 | ||
| # This program is free software: you can redistribute it and/or modify it under
 | ||
| # the terms of the GNU Affero General Public License as published by the Free
 | ||
| # Software Foundation, either version 3 of the License, or (at your option) any
 | ||
| # later version.
 | ||
| # 
 | ||
| # This program is distributed in the hope that it will be useful, but WITHOUT
 | ||
| # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | ||
| # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 | ||
| # details.
 | ||
| #
 | ||
| # You should have received a copy of the GNU Affero General Public License along
 | ||
| # with this program. If not, see https://www.gnu.org/licenses/.
 | ||
| 
 | ||
| set -e
 | ||
| 
 | ||
| test -z "$DEBUG" || set -x
 | ||
| 
 | ||
| test -z "$XDG_CACHE_HOME" && cachefile="$HOME/.cache/yt.cache" \
 | ||
| 	|| cachefile="$XDG_CACHE_HOME/yt.cache"
 | ||
| 
 | ||
| test -n "$YT_PL_DIR" \
 | ||
| 	|| test -n "$XDG_DATA_HOME" && YT_PL_DIR="$XDG_DATA_HOME/yt" \
 | ||
| 	|| YT_PL_DIR="$HOME/.local/share/yt"
 | ||
| 
 | ||
| argv0="$0"
 | ||
| com="$1"
 | ||
| 
 | ||
| FMT='%(title)s – %(channel)s (%(duration>%H:%M:%S)s) [%(webpage_url)s]'
 | ||
| WBAPI='https://archive.org/wayback/available?url='
 | ||
| 
 | ||
| 
 | ||
| add() { # adds a video to a playlist file
 | ||
| 	while test -f "$YT_PL_DIR/$2.m3u"; do
 | ||
| 		file="$YT_PL_DIR/$2.m3u"
 | ||
| 		video="$(printf '%s\n' "$1" \
 | ||
| 			| sed -e 's/youtu\.be\//www.youtube\.com\/watch?v=/g' \
 | ||
| 			-e 's/\?[^v].*$//g')"
 | ||
| 
 | ||
| 		if test -n "$(grep -e "$video" "$file")"
 | ||
| 		then
 | ||
| 			printf "%s: %s: %s: Video already in playlist.\n" \
 | ||
| 				"$argv0" "$file" "$video"
 | ||
| 			exit
 | ||
| 		else
 | ||
| 			cache "$video"
 | ||
| 			archive "$video"
 | ||
| 			printf "%s\n" "$video" >> "$file"
 | ||
| 		fi
 | ||
| 
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| archive() { # archives a video to the Wayback Machine
 | ||
| 	while test -n "$1"; do
 | ||
| 		if test -n "$(printf '%s\n' "$1" | grep -e 'web\.archive\.org')"
 | ||
| 		then
 | ||
| 			shift
 | ||
| 			continue
 | ||
| 		else
 | ||
| 			wayback_url="$(curl -s "$WBAPI$1" \
 | ||
| 					| jq .archived_snapshots.closest.url \
 | ||
| 					| tr -d '"' | sed 's/^http:/https:/g')"
 | ||
| 
 | ||
| 			# skips archiving on the Wayback Machine if the video has a snapshot
 | ||
| 			if [ "$wayback_url" != "null" ]; then shift; continue; fi
 | ||
| 
 | ||
| 			printf "%s: %s: Saving video to the Wayback Machine.\n" "$argv0" "$1" 1>&2
 | ||
| 			curl -s -d "url=$1" 'https://web.archive.org/{url}' >/dev/null
 | ||
| 		fi
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| cache() { # cache the video title for faster retrieval
 | ||
| 	test -e "$cachefile" || touch "$cachefile"
 | ||
| 
 | ||
| 	while test -n "$1"; do
 | ||
| 		grep "$1" "$cachefile" 2>/dev/null 1>&2 || printf '%s\n' \
 | ||
| 			"$(yt-dlp -s "$1" --print "$FMT")" >>"$cachefile"
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| clone() { # clones a YouTube playlist to a file
 | ||
| 	test -d "$YT_PL_DIR" || mkdir -p "$YT_PL_DIR"
 | ||
| 
 | ||
| 	test -n "$2" && file="$2" \
 | ||
| 		|| file="$YT_PL_DIR/$(yt-dlp -s -I '1:1' --print '%(playlist)s' "$1").m3u"
 | ||
| 
 | ||
| 	yt-dlp --flat-playlist "$1" --print url > "$file"
 | ||
| 	verify "$file"
 | ||
| }
 | ||
| 
 | ||
| lines() {
 | ||
| 	while test -n "$1"; do
 | ||
| 		sed 's/#.*$//g' "$1"
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| list() {
 | ||
| 	if test -z "$YTPICK"; then
 | ||
| 		printf "%s: Please set \$YTPICK to your preferred picking tool." \
 | ||
| 			"$argv0" 1>&2
 | ||
| 		exit 78 # sysexits.h(3) EX_CONFIG
 | ||
| 	fi
 | ||
| 
 | ||
| 	choices="$(menu)"
 | ||
| 
 | ||
| 	test -z "$choices" || pick "$choices"
 | ||
| }
 | ||
| 
 | ||
| menu() {
 | ||
| 		playlist="$(ls "$YT_PL_DIR" | sed 's/\.m3u//g' | $YTPICK)"
 | ||
| 
 | ||
| 		while test -d "$YT_PL_DIR/$playlist"; do
 | ||
| 			dir="$playlist"
 | ||
| 			playlist="$(ls "$YT_PL_DIR/$playlist" | sed 's/\.m3u//g' | $YTPICK \
 | ||
| 				|| printf '')"
 | ||
| 		done
 | ||
| 
 | ||
| 		printf '%s/%s\n' "$dir" "$playlist"
 | ||
| }
 | ||
| 
 | ||
| music() { # downloads a video, splitting by chapter and only saving the audio
 | ||
| 	while test -n "$1"; do
 | ||
| 		yt-dlp -vx --split-chapters -o \
 | ||
| 			"chapter:%(fulltitle)s - %(section_number)s %(section_title)s.%(ext)s" \
 | ||
| 			"$1" --audio-quality 0 >> log 2>&1
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| pick() { # Pick a video to play from a playlist of videos
 | ||
| 	if test -z "$YTPICK"; then
 | ||
| 		printf "%s: Please set \$YTPICK to your preferred picking tool." \
 | ||
| 			"$argv0" 1>&2
 | ||
| 		exit 78 # sysexits.h(3) EX_CONFIG
 | ||
| 	fi
 | ||
| 
 | ||
| 	if test -f "$YT_PL_DIR/$1.m3u"; then
 | ||
| 		file="$YT_PL_DIR/$1.m3u"
 | ||
| 
 | ||
| 		for line in $(lines "$file"); do
 | ||
| 			if test -n "$(printf '%s\n' "$line" | sed -n '/^\#/p')"
 | ||
| 			then
 | ||
| 				continue
 | ||
| 			else
 | ||
| 				cache "$line"
 | ||
| 			fi
 | ||
| 
 | ||
| 			if test -z "$list"; then
 | ||
| 				list="$(grep "$line" "$cachefile")"
 | ||
| 			else
 | ||
| 				list="$(printf '%s\n%s' "$list" "$(grep "$line" "$cachefile")")"
 | ||
| 			fi
 | ||
| 		done
 | ||
| 
 | ||
| 		chosen="$(printf '%s\n' "$list" \
 | ||
| 			| $YTPICK | sed -e 's/.*\[//g' -e 's/\]//g')"
 | ||
| 
 | ||
| 		test -z "$chosen" \
 | ||
| 			|| printf "%s: %s: Playing stream.\n" "$argv0" "$chosen" 1>&2 \
 | ||
| 			&& mpv "$chosen"
 | ||
| 	fi
 | ||
| }
 | ||
| 
 | ||
| play() { # play a video after caching its title
 | ||
| 	cache "$@"
 | ||
| 	mpv "$@"
 | ||
| }
 | ||
| 
 | ||
| queue() {
 | ||
| 	while test -f "$YT_PL_DIR/$1.m3u"; do
 | ||
| 		mpv "$YT_PL_DIR/$1.m3u"
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| search() {
 | ||
| 	if test -z "$YTPICK"; then
 | ||
| 		printf "%s: Please set \$YTPICK to your preferred picking tool." \
 | ||
| 			"$argv0" 1>&2
 | ||
| 		exit 78 # sysexits.h(3) EX_CONFIG
 | ||
| 	fi
 | ||
| 
 | ||
| 	results="$(yt-dlp "ytsearch$2:$1" --print "$FMT")"
 | ||
| 
 | ||
| 	selection="$(printf '%s\n' "$results" \
 | ||
| 		| $YTPICK \
 | ||
| 		| sed 's/.*\[//g' \
 | ||
| 		| tr -d ']')"
 | ||
| 
 | ||
| 	if test -n "$selection"; then
 | ||
| 		option="$(printf 'copy\nmusic\nplay\nsave\n' | $YTPICK)"
 | ||
| 
 | ||
| 		case "$option" in
 | ||
| 			"copy")
 | ||
| 				wl-copy "$selection"
 | ||
| 				;;
 | ||
| 			"music")
 | ||
| 				music "$selection"
 | ||
| 				;;
 | ||
| 			"play")
 | ||
| 				play "$selection"
 | ||
| 				;;
 | ||
| 			"save")
 | ||
| 				add "$selection" "$(menu)"
 | ||
| 				;;
 | ||
| 		esac
 | ||
| 	fi
 | ||
| }
 | ||
| 
 | ||
| usage() {
 | ||
| 	printf "Usage: %s %s\n" "$argv0" "$@" 1>&2
 | ||
| 	exit 64 # sysexits.h(3) EX_USAGE
 | ||
| }
 | ||
| 
 | ||
| verify() { # replaces videos with archived versions if they are not available
 | ||
| 	while test -f "$YT_PL_DIR/$1.m3u"; do
 | ||
| 		file="$YT_PL_DIR/$1.m3u"
 | ||
| 
 | ||
| 		for video in $(lines "$file"); do
 | ||
| 			if test -n "$(yt-dlp -s "$video" 2>&1 \
 | ||
| 				| grep -i -e 'video unavailable' -e 'private video' -e 'been removed')"
 | ||
| 			then
 | ||
| 				printf "%s: %s: Video unavailable.\n" "$argv0" "$video" 1>&2
 | ||
| 
 | ||
| 				wayback_url="$(curl -s "$WBAPI$video" \
 | ||
| 					| jq -r .archived_snapshots.closest.url \
 | ||
| 					| sed 's/^http:/https:/g')"
 | ||
| 
 | ||
| 				if [ "$wayback_url" = "null" ] \
 | ||
| 					|| test -n "$(yt-dlp -s "$wayback_url" 2>&1 | grep -e 'not archived')"
 | ||
| 				then
 | ||
| 					printf "%s: %s: Video not available on the Wayback Machine.\n" \
 | ||
| 						"$argv0" "$video" 1>&2
 | ||
| 					# replace the video link with a comment containing link
 | ||
| 					content="$(sed "s;^$video;\# $video;g" "$file")"
 | ||
| 
 | ||
| 					# write the new content buffer to file
 | ||
| 					printf '%s\n' "$content" > "$file"
 | ||
| 				else
 | ||
| 					printf "%s: %s: Replacing link with Wayback link.\n" \
 | ||
| 						"$argv0" "$video" 1>&2
 | ||
| 
 | ||
| 					content="$(sed "s;^$video;$wayback_url;g" "$file")"
 | ||
| 					printf '%s\n' "$content" >"$file"
 | ||
| 					cache "$wayback_url"
 | ||
| 				fi
 | ||
| 			else
 | ||
| 				printf '%s: %s: Video available.\n' "$argv0" "$video" 1>&2
 | ||
| 				cache "$video"
 | ||
| 				archive "$video"
 | ||
| 			fi
 | ||
| 		done
 | ||
| 		
 | ||
| 		shift
 | ||
| 	done
 | ||
| }
 | ||
| 
 | ||
| for dep in \
 | ||
| 	curl \
 | ||
| 	jq \
 | ||
| 	mpv \
 | ||
| 	yt-dlp
 | ||
| do
 | ||
| 	if ! command -v "$dep" >/dev/null
 | ||
| 	then
 | ||
| 		printf "%s: %s: Missing dependency." "$argv0" "$dep" 1>&2
 | ||
| 		exit 69 # syexits.h(3) EX_UNAVAILABLE
 | ||
| 	fi
 | ||
| done
 | ||
| 
 | ||
| case "$com" in
 | ||
| 	add)
 | ||
| 		shift 2>/dev/null || usage 'add uri file'
 | ||
| 		add "$@"
 | ||
| 		;;
 | ||
| 	archive)
 | ||
| 		shift 2>/dev/null || usage 'archive uri...'
 | ||
| 		archive "$@"
 | ||
| 		;;
 | ||
| 	cache)
 | ||
| 		shift 2>/dev/null || usage 'cache uri...'
 | ||
| 		cache "$@"
 | ||
| 		;;
 | ||
| 	clone)
 | ||
| 		shift 2>/dev/null || usage 'clone uri [file]'
 | ||
| 		clone "$@"
 | ||
| 		;;
 | ||
| 	list)
 | ||
| 		shift 2>/dev/null || usage 'list'
 | ||
| 		list "$@"
 | ||
| 		;;
 | ||
| 	music)
 | ||
| 		shift 2>/dev/null || usage 'music uri...'
 | ||
| 		music "$@"
 | ||
| 		;;
 | ||
| 	pick)
 | ||
| 		shift 2>/dev/null || usage 'pick file...'
 | ||
| 		pick "$@"
 | ||
| 		;;
 | ||
| 	play)
 | ||
| 		shift 2>/dev/null || usage 'play uri...'
 | ||
| 		play "$@"
 | ||
| 		;;
 | ||
| 	queue)
 | ||
| 		shift 2>/dev/null || usage 'queue file...'
 | ||
| 		queue "$@"
 | ||
| 		;;
 | ||
| 	search)
 | ||
| 		shift 2>/dev/null || usage 'search term [count]'
 | ||
| 		search "$@"
 | ||
| 		;;
 | ||
| 	verify)
 | ||
| 		shift 2>/dev/null || usage 'file...'
 | ||
| 		verify "$@"
 | ||
| 		;;
 | ||
| 	*)
 | ||
| 		usage 'add | clone | list | music | pick | play | verify'
 | ||
| 		;;
 | ||
| esac
 |