raspberry-pi - 使用命令行参数调用 CCL + Quicklisp 脚本作为可执行文件并实现所需的输出

标签 raspberry-pi lisp common-lisp quicklisp

在发现一种使用我的新 Raspberry Pi 2(运行 Raspbian)的命令行从命令行观看 YouTube 视频的非常简单的方法后,仅使用易于获得的软件包,即:

omxplayer -o local $(youtube-dl -g {videoURL})

我立即想要一种以这种方式观看整个 YouTube 播放列表的方法。所以我认为这是在 Common Lisp 中拼凑解决方案的完美借口。 :)

我的解决方案(富有想象力地称为 RpiTube)是一个脚本,当给定 YouTube 播放列表的 URL 时,它会搜索页面的 HTML 源并提取其中包含的视频的 URL。那我可以 将这些 URL 传递给 Bash 脚本,该脚本最终会一个接一个地为每个视频单独调用上述命令。 Common Lisp 脚本本身是完整的并且可以工作,但是我很难用 URL 作为命令行参数来调用它。这主要是因为我对 Quicklisp、Lisp 包和从 Common Lisp 代码创建可执行文件还很陌生。

我在跑 Clozure Common Lisp (CCL) 和 Quicklisp(根据 Rainer Joswig's instructions 安装)。我在下面包含了完整的代码。它可能有点低效,但令我惊讶的是它甚至在 Raspberry Pi 上运行得相当快。 (建议的改进表示赞赏。)

;rpitube.lisp

;Given the URL of a YouTube playlist's overview page, return a list of the URLs of videos in said playlist.

(load "/home/pi/quicklisp/setup.lisp")
(ql:quickload :drakma)
(ql:quickload "cl-html-parse")
(ql:quickload "split-sequence")

(defun flatten (x)
  "Paul Graham's utility function from On Lisp."
  (labels ((rec (x acc)
             (cond ((null x) acc)
                   ((atom x) (cons x acc))
                   (t (rec (car x) (rec (cdr x) acc))))))
    (rec x nil)))

(defun parse-page-source (url)
  "Generate lisp list of a page's html source."
  (cl-html-parse:parse-html (drakma:http-request url)))

(defun occurences (e l)
  "Returns the number of occurences of an element in a list. Note: not fully tail recursive."
  (cond
    ((null l) 0)
    ((equal e (car l)) (1+ (occurences e (cdr l))))
    (t (occurences e (cdr l)))))

(defun extract-url-stubs (flatlist unique-atom url-retrieval-fn)
  "In a playlist's overview page the title of each video is represented in HTML as a link,
  whose href entry is part of the video's actual URL (referred to here as a stub).
  Within the link's tag there is also an entry that doesn't occur anywhere else in the
  page source. This is the unique-atom (a string) that we will use to locate the link's tag
  within the flattened list of the page source, from which we can then extract the video's URL
  stub using a simple url-retrieval-fn (see comments below this function). This function is iterative, not
  recursive, because the latter approach was too confusing."
  (let* ((tail (member unique-atom flatlist :test #'equal))
         (n (occurences unique-atom tail))
         (urls nil))
    (loop for x in tail with i = 0
          while (< (length urls) n) do
          (if (string= x unique-atom)
              (setf urls (cons (funcall url-retrieval-fn tail i) urls)))
          (incf i))
    (reverse urls)))

;Example HTML tag:
;<a class="pl-video-title-link yt-uix-tile-link yt-uix-sessionlink  spf-link " data-sessionlink="verylongirrelevantinfo" href="/watch?v=uniquevideocode&index=numberofvideoinplaylist&list=uniqueplaylistcode" dir="ltr"></a>

;Example tag when parsed and flattened:
;(:A :CLASS "pl-video-title-link yt-uix-tile-link yt-uix-sessionlink  spf-link " :DATA-SESSIONLINK "verylongirrelevantinfo" :HREF "/watch?v=uniquevideocode&index=numberofvideoinplaylist&list=uniqueplaylistcode" :DIR "ltr")

;The URL stub is the fourth list element after unique-atom ("pl-video-title..."), so the url-retreival-fn is:
;(lambda (l i) (elt l (+ i 4))), where i is the index of unique-atom.

(defun get-vid-urls (url)
  "Extracts the URL stubs, turns them into full URLs, and returns them in a list."
  (mapcar (lambda (s)
            (concatenate 'string
                         "https://www.youtube.com"
                         (car (split-sequence:split-sequence #\& s))))
          (extract-url-stubs (flatten (parse-page-source url))
                             "pl-video-title-link yt-uix-tile-link yt-uix-sessionlink  spf-link "
                             (lambda (l i) (elt l (+ i 4))))))

(let ((args #+clozure *unprocessed-command-line-arguments*))
(if (and (= (length args) 1)
         (stringp (car args)))
    (loop for url in (get-vid-urls (car args)) do
          (format t "~a " url))
    (error "Usage: rpitube <URL of youtube playlist>

           where URL is of the form:
           'https://www.youtube.com/playlist?list=uniqueplaylistcode'")))

首先我尝试在脚本中添加以下行

#!/home/pi/ccl/armcl

然后运行

$ chmod +x rpitube.lisp
$ ./rpitube.lisp {playlistURL}

给出:

Unrecognized non-option arguments: (./rpitube.lisp {playlistURL})

当我至少期望 ./rpitube.lisp 不在这个无法识别的参数列表中时。我知道在 Clozure CL 中,为了将命令行参数传递给 REPL session 不变,我必须用双连字符将它们与其他参数分开,如下所示:

~/ccl/armcl -l rpitube.lisp -- {playlistURL}

但是像这样调用脚本显然会在脚本运行后让我进入 REPL,这是我不想要的。此外,Quicklisp 加载信息和进度条会打印到终端,这也是我不想要的。 (顺便说一下,正如 Rainer 所建议的,我没有将 Quicklisp 添加到我的 CCL 初始化文件中,因为我通常不想要额外的开销,即 Raspberry Pi 上几秒钟的加载时间。我不确定这是否相关)。

然后我决定尝试通过运行(加载上述代码后)来创建一个独立的可执行文件:

(ccl:save-application "rpitube" :prepend-kernel t)

然后像这样从 shell 中调用它:

$ ./rpitube {playlistURL}

给出:

Unrecognized non-option arguments: ({playlistURL})

这似乎是一个改进,但我仍然做错了。我是否需要通过创建我自己的需要 drakma、cl-html-extract 和 split-sequence 的 asdf 包并使用 in-package 等加载它来替换与 Quicklisp 相关的代码?我之前在另一个项目中创建了自己的包 - 特别是因为我想将我的代码拆分成多个文件 - 它似乎可以工作,但我仍然通过 ql:quickload 加载我的包,而不是in-package,因为后者似乎从来没有工作过(也许我应该把它作为一个单独的问题来问)。在这里,rpitube.lisp 代码非常短,似乎没有必要为它创建一个完整的 quickproject 和包,尤其是因为我希望它是一个独立的可执行文件。

那么:我该如何更改脚本(或其调用),以便它可以接受 URL 作为命令行参数,可以非交互方式运行(即不打开 REPL),并且只打印所需的终端输出 - 以空格分隔的 URL 列表 - 没有任何 Quicklisp 加载信息?

最佳答案

好的,我已经设法根据上面用户@m-n 链接的建议改编了一个解决方案。 RpiTube 现在似乎适用于我尝试过的大多数播放列表,除了一些音乐播放列表,这些播放列表是不可靠的,因为我住在德国,而且许多音乐视频在这个国家出于法律原因被屏蔽。巨大的播放列表、非常高质量(或非常长)的视频可能不可靠。

BASH 脚本:

#! /bin/bash

#Calls rpitube.lisp to retrieve the URLs of the videos in the provided
#playlist, and then plays them in order using omxplayer, optionally
#starting from the nth video instead of the first.

CCL_PATH='/home/pi/ccl/armcl'
RPITUBE_PATH='/home/pi/lisp/rpitube.lisp'
N=0
USAGE='
Usage: ./rpitube [-h help] [-n start at nth video] <playlist URL>

       where URL is of the form: https://www.youtube.com/playlist?list=uniqueplaylistcode
       ******** Be sure to surround the URL with single quotes! *********'

play()
{
  if `omxplayer -o local $(youtube-dl -g "$1") > /dev/null`; then
    return 0
  else
    echo "An error occured while playing $1."
    exit 1
  fi
}

while getopts ":n:h" opt; do
  case $opt in
    n ) N=$((OPTARG - 1)) ;;
    h ) echo "$USAGE"
        exit 1 ;;
    \? ) echo "Invalid option."
         echo "$USAGE"
         exit 1 ;;
  esac
done

shift $(($OPTIND - 1))

if [[ "$#" -ne 1 ]]; then
  echo "Invalid number of arguments."
  echo "$USAGE"
  exit 1
elif [[ "$1" != *'https://www.youtube.com/playlist?list='* ]]; then
  echo "URL is of the wrong form."
  echo "$USAGE"
  exit 1
else
  echo 'Welcome to RpiTube!'
  echo 'Fetching video URLs... (may take a moment, especially for large playlists)'
  urls="$(exec $CCL_PATH -b -e '(progn (load "'$RPITUBE_PATH'") (main "'$1'") (ccl::quit))')"
  echo 'Starting video... press Q to skip to next video, left/right arrow keys to rewind/fast-forward, Ctrl-C to quit.'
  count=0
  for u in $urls; do           #do NOT quote $urls here
    [[ $count -lt $N ]] && count=$((count + 1)) && continue
    play "$u"
    echo 'Loading next video...'
  done
  echo 'Reached end of playlist. Hope you enjoyed it! :)'
fi

我对 CL 脚本进行了以下更改:将 :silent 选项添加到 ql:quickload 调用中;用内置的 count (:test #'equal) 替换我自己的 occurrences 函数;最重要的是,脚本末尾的代码实际上调用了 URL 获取函数。首先,我将它包装在一个接受一个参数的 main 函数中,即播放列表 URL,并删除了对 *command-line-argument-list* 等的引用。重要的部分:我没有使用 URL 作为 CCL 的命令行参数来调用整个 rpitube.lisp 脚本,而是在没有参数的情况下调用它,而是将 URL 传递给 main直接函数(在对 exec 的调用中)。见下文:

(defun main (url)
  (if (stringp url)
      (loop for u in (get-vid-urls url) do
            (format t "~a " u))
      (error "Usage: rpitube <URL of youtube playlist>

              where URL is of the form:
              'https://www.youtube.com/playlist?list=uniqueplaylistcode'")))

这种方法可以广泛应用并且效果很好,但如果没有更好的方法,我会感到惊讶。如果我可以在“顶层”功能 + 可执行想法方面取得任何进展,我将编辑此答案。

一个工作调用示例,在短视频的小型播放列表上运行,从第 3 个视频开始播放:

$ ./rpitube -n 3 'https://www.youtube.com/playlist?list=PLVPJ1jbg0CaE9eZCTWS4KxOWi3NWv_oXL'

非常感谢。

关于raspberry-pi - 使用命令行参数调用 CCL + Quicklisp 脚本作为可执行文件并实现所需的输出,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30789161/

相关文章:

linux - 树莓派上的内存

bash - 我可以在 raspbian 上的树莓派上运行带有 webmin 界面的 LAMP 服务器吗?

clojure - 在 clojure 中,什么时候定义多个名称相同但元数据不同的符号有用?

bluetooth - 低功耗蓝牙 (BLE) 数据传输中的持续延迟

python - 嵌入式平台OpenCV的特征检测

lisp - #+: and #-: in common lisp是什么意思

recursion - 我如何在 LISP 中创建一个递归函数来计算一个原子在嵌套列表中出现的次数

tabs - Common-Lisp 以函数格式打印制表符

lisp - 列表的 CLISP 表示

macros - ALET 宏和间接使用