mpv-clip 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. #!/bin/bash
  2. usage() {
  3. if [[ "$@" != "" ]]; then
  4. echo_term "$@"
  5. notify "$@" "Help text in terminal: $me -h"
  6. fi
  7. echo_term "
  8. Play videos from URLs with mpv+yt-dlp (youtube-dl). Example:
  9. mpv-clip \"\$(xclip -o)\"
  10. Will copy the URL back to the clipboard or primary selection, formatted with
  11. media title.
  12. Currently yt-dlp is recommend instead of youtube-dl (can be configured in
  13. ~/.config/mpv/mpv.conf with 'script-opts=ytdl_hook-ytdl_path=yt-dlp').
  14. Without any youtube-dl compatible, mpv can only deal with direct media URLs.
  15. Notifications are sent by default, using $me for both app and icon name.
  16. Options:
  17. -a Play only audio, but open a small controllable window with elapsed/total
  18. time, and don't disable the screensaver.
  19. -p Advance in playlist - does not seem to work with youtube currently.
  20. Does work with e.g. bandcamp.
  21. -m int Define desired maximum height of video (not always available).
  22. The best available audio is always chosen!
  23. Please keep in mind that youtube-dl always defaults to the best
  24. available quality unless specified. Overrides -f.
  25. -f int youtube-dl format. Example: 22. No default. Overrides -m.
  26. -y Replaces youtube with invidious instance (dynamically fetched from
  27. $ivio)
  28. Fallback: $ytmirror_fallback
  29. Requires ${ytmirrordeps[@]}.
  30. -r str Comma-separated list of prefixes to remove from URL
  31. -s str Override list of subtitle languages to embed. The default is to use
  32. your locale + English (2 letter ISO 639-1 codes): \"${slang}\". Pass empty
  33. string \"\" for ALL subtitles; can be a lot for e.g. youtube!
  34. -c str How to copy URL+title back to the clipboard - this string must contain
  35. exactly two letters:
  36. 1. c|p|b for clipboard, primary selection, or both
  37. 2. b|m|t for BBCode, markdown or plain text
  38. Default: $clipboard$markup.
  39. -t int Network timeout in seconds.
  40. -N Do NOT send desktop notifications.
  41. -h This text.
  42. "
  43. exit 1
  44. }
  45. notify() {
  46. [[ "$notify" == 1 ]] && notify-send -a "$me" -i "$me" -- "$@" || \
  47. echo_term "$@"
  48. }
  49. echo_term() {
  50. [ -t 1 ] && echo -e "$@"
  51. }
  52. clipboard() {
  53. [[ "$clipboard" == "" ]] && return
  54. # variables URL and title must be defined
  55. case $markup in
  56. b) out="[url=${URL}]${title}[/url]"
  57. ;;
  58. m) out="[${title}](${URL})"
  59. ;;
  60. t) out="$URL --- $title"
  61. ;;
  62. esac
  63. case $clipboard in
  64. c) echo "$out" | xsel -b
  65. ;;
  66. p) echo "$out" | xsel -p
  67. ;;
  68. b) echo "$out" | xsel -b
  69. echo "$out" | xsel -p
  70. ;;
  71. esac
  72. }
  73. me="${0##*/}" # xprop class name to be used by xdotool and optionally window manager
  74. bbcode=0 # fill clipboard with BBcode instead of plain text
  75. notify=1
  76. timeout=5 # network timeout in seconds
  77. geometry="33%x33%+0-0"
  78. maxheight=''
  79. clipboard='c'
  80. cbtext="clipboard"
  81. markup='b'
  82. yt_format=''
  83. ivio="https://api.invidious.io/?sort_by=type,health"
  84. ytmirror_fallback="invidious.snopyta.org"
  85. ytmirror=0
  86. ytmirrordeps=(curl xmllint)
  87. prefixes=()
  88. for dep in mpv; do
  89. type "$dep" >/dev/null || usage
  90. done
  91. slang="${LANG%%_*}"
  92. slang="${slang,,}";
  93. [[ "$slang" == en ]] || slang="${slang},en"
  94. # Just to initialize the array...
  95. opts=( --force-window=yes ) # mpv options for all use cases
  96. while getopts apm:f:ys:r:c:t:i:hN opt; do
  97. case $opt in
  98. a) geometry="33%x30+0+0"
  99. opts=( ${opts[@]} --vid=no --sub=no
  100. --osd-scale-by-window=no --no-osd-bar --no-osc
  101. --osd-msg1=\${time-pos}/\${duration}
  102. --osd-font-size=28 --osd-font=monospace --background=.1/.1/.1
  103. --osd-color=.9/.9/.9 --osd-border-size=0
  104. --osd-margin-y=0 --osd-margin-x=5 --no-stop-screensaver )
  105. me="${me}-audioonly"
  106. ;;
  107. p) opts=( ${opts[@]} --ytdl-raw-options=ignore-errors=,yes-playlist= ) # as of 2020-10-19, this does not seem to work at all with youtube (or invidious mirrors)
  108. me="${me}-playlist"
  109. ;;
  110. m)
  111. [ "$OPTARG" -lt 144 ] || [ "$OPTARG" -gt 6912 ] && usage "-$opt: Invalid number: $OPTARG"
  112. maxheight="$OPTARG"
  113. yt_format=""
  114. ;;
  115. f)
  116. (( OPTARG >= 0 )) || usage "-$opt: Invalid number: $OPTARG"
  117. yt_format="$OPTARG"
  118. maxheight=""
  119. ;;
  120. y) ytmirror=1
  121. for dep in "${ytmirrordeps[@]}"; do type $dep >/dev/null || usage; done
  122. ;;
  123. s) slang="$OPTARG" # comma separated list of subtitle languages - pass empty string '' or "" to get ALL subtitles
  124. ;;
  125. r) # comma separated list of prefixes to remove from URL converted to array
  126. mapfile -d, -t prefixes <<<"$OPTARG"
  127. prefixes[-1]="${prefixes[-1]%$'\n'}" # remove last char, which is a newline
  128. ;;
  129. c) (( "${#OPTARG}" != 2 )) && usage "Invalid argument -$opt $OPTARG"
  130. clipboard="${OPTARG:0:1}"
  131. markup="${OPTARG:1:1}"
  132. [[ "$clipboard" =~ ^(c|p|b)$ ]] && [[ "$markup" =~ ^(b|m|t)$ ]] || usage "Invalid argument -$opt $OPTARG"
  133. type xsel >/dev/null || usage
  134. case $clipboard in
  135. c) cbtext="clipboard" ;;
  136. p) cbtext="primary selection" ;;
  137. b) cbtext="clipboard and primary selection" ;;
  138. esac
  139. ;;
  140. t) timeout="$OPTARG"
  141. [[ "$OPTARG" =~ ^[1-9][0-9]*$ ]] || usage "-$opt: Invalid number: $OPTARG (must be a positive integer)"
  142. ;;
  143. i) icon="$OPTARG"
  144. [ -r "$OPTARG" ] || usage "-$opt: Cannot read $OPTARG"
  145. ;;
  146. N) notify=0
  147. ;;
  148. h|*) usage
  149. ;;
  150. esac
  151. done
  152. shift $((OPTIND-1))
  153. cbtext="(copied URL+title to $cbtext)"
  154. # first things first...
  155. URL="$1"
  156. # remove leading & trailing whitespace:
  157. URL="${URL%[[:space:]]*}"
  158. URL="${URL#[[:space:]]*}"
  159. # Remove prefixes
  160. for pre in "${prefixes[@]}"; do
  161. echo removed "$pre"
  162. URL="${URL#$pre}"
  163. echo "$URL"
  164. done
  165. [[ "$URL" != http?(s)://?(www.)* ]] && usage "$URL is not a valid URL"
  166. # silently drop notifications if this is not available
  167. type notify-send >/dev/null 2>&1 || [[ "$notify" == 0 ]]
  168. if [[ "$ytmirror" == 1 ]]; then
  169. entry=1 # use first entry, or second, etc.
  170. # get the mirror
  171. ytmirror="$(curl "$ivio" | xmllint --html --xpath "//div[@class=\"content\"]/table/tbody/tr[$entry]/td[1]/a/@href" - )"
  172. ytmirror="${ytmirror##*https://}"
  173. ytmirror="${ytmirror%%\"*}"
  174. ytmirror="${ytmirror%/}" # if there is a trailing slash, remove it
  175. curl -I "$ytmirror" || ytmirror="$ytmirror_fallback"
  176. ytdomains=( www.youtube.com youtube.com m.youtube.com youtu.be )
  177. for domain in "${ytdomains[@]}"; do
  178. if [[ "$URL" == http?(s)://$domain/* ]]; then
  179. #~ if [[ "$ytmirror" != "" ]] && host -W"$timeout" "$ytmirror" && ping -c1 -n -w"$timeout" "$ytmirror"; then
  180. URL="${URL/$domain/$ytmirror}" && break
  181. #~ else
  182. #~ ytmirror=''
  183. #~ fi >/dev/null
  184. #~ break
  185. fi
  186. done
  187. fi
  188. # introducing raw options passed to youtube-dl
  189. [[ "$me" == *"-audioonly" ]] && ytdlrawopts="format=bestaudio" || {
  190. [[ "$maxheight" != "" ]] && ytdlrawopts="format=bestvideo[height<=$maxheight]+bestaudio/best"
  191. [[ "$yt_format" != "" ]] && ytdlrawopts="format=$yt_format"
  192. }
  193. [[ "$maxheight" != "" ]] && ytdlrawopts="${ytdlrawopts},"
  194. # we will restrict video resolution whether the user explicitely requests it or not
  195. ytdlrawopts="${ytdlrawopts}socket-timeout=$timeout"
  196. [[ "$slang" == "" ]] || ytdlrawopts="${ytdlrawopts},sub-lang=\"$slang\""
  197. # for indentifying windows (and notifications)
  198. me="$me-$(printf '%(%H:%M:%S)T')"
  199. # Building the command line
  200. opts=( ${opts[@]}
  201. --network-timeout=$timeout
  202. --geometry="$geometry"
  203. --ytdl --script-opts=try_ytdl_first=yes
  204. --ytdl-raw-options="$ytdlrawopts"
  205. --x11-name="$me"
  206. --quiet
  207. --no-msg-color
  208. --no-input-terminal --term-playing-msg='${media-title}'
  209. --no-resume-playback
  210. --watch-later-directory=/dev/null
  211. )
  212. notify "Trying: $URL" "$me"
  213. notify coproc mpv "${opts[@]}" "$URL"
  214. coproc mpv "${opts[@]}" "$URL"
  215. error=0
  216. while read -r -u "${COPROC[0]}" line; do
  217. echo "$line"
  218. if [[ "$line" == "term-msg: "* ]]; then
  219. title="${line#*: }"
  220. notify "$title" "$me $cbtext"
  221. clipboard
  222. #~ often there are http errors, but playback continues regardless. so let's drop it.
  223. #~ elif [[ "$line" == *"HTTP error"* ]] || [[ "$line" == *"ytdl_hook: ERROR:"* ]]; then
  224. elif [[ "$line" == *"ytdl_hook: ERROR:"* ]]; then
  225. line="${line%%; please report this issue*}"
  226. notify "$line" "$me" && error=1
  227. fi
  228. done
  229. [[ "$error" == 1 ]] && exit 1
  230. clipboard
  231. notify "That was: $title" "Exiting $me $cbtext"