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

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

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

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

我立刻想要一种方式来观看整个YouTube播放列表。因此,我认为这是在Common Lisp中合并解决方案的完美借口:)

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

我正在使用Quicklisp运行Clozure Common Lisp(CCL)(根据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会话不变,我必须用双连字符将它们与其他参数分开,如下所示:

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

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

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

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

从像这样的shell中调用它:

$ ./rpitube {playlistURL}

给出:

Unrecognized non-option arguments: ({playlistURL})

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

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

最佳答案:

2 个答案:

答案 0 :(得分:1)

我看了一下,想分享我发现的东西。还有一些Lisp库旨在促进脚本编写,可执行构建或命令行参数处理。

对于可执行构建方法,save-application允许您指定:toplevel-function,即零参数的函数。在这种情况下,您需要通过ccl:*command-line-argument-list*获取命令行参数,并跳过第一个元素(程序的名称)。这可能是让你的程序运行的最小变化(我没有运行它;所以它可能有拼写错误):

(defun toplevel ()
  (let ((args # clozure *command-line-argument-list*))
    (if (and (= (length args) 2)
             (stringp (second args)))
        (loop for url in (get-vid-urls (second 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'"))))

(save-application "rpitube" :prepend-kernal t :toplevel-function #'toplevel)

或者,一些Lisp实现具有--scpript命令行参数,该参数允许类似于您的#!/home/pi/ccl/armcl脚本的工作。 CCL似乎没有相同的选项,但之前的答案 - https://stackoverflow.com/a/3445196/2626993 - 建议编写一个简短的Bash脚本,其行为基本上就像你希望CCL会尝试这样。

quickload来电可以通过参数沉默:

  (ql:quickload :drakma :silent t)

答案 1 :(得分:1)

好的,我已经设法从上面用户@ 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 "") > /dev/null`; then
    return 0
  else
    echo "An error occured while playing ."
    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 [[ "" != *'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 "''") (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次调用;用内置的ocurrencescount)替换我自己的:test #'equal函数;最重要的是,脚本末尾的代码实际上调用了URL提取函数。首先,我将它包装在main函数中,该函数接受一个参数,即播放列表URL,并删除对*command-line-argument-list*等的引用。重要的部分:而不是调用整个rpitube.lisp脚本URL作为CCL的命令行参数,我在没有参数的情况下调用它,而是直接将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'

非常感谢。

DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
U19学习网站 » raspberry-pi - 使用命令行参数将CCL &#43; Quicklisp脚本调用为可执行文件,并实现所需的输出