在发现了一种非常简单的方法来使用我的新 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 代码创建可执行文件还很陌生。
我正在运行带有 Quicklisp 的Clozure Common Lisp (CCL)(按照Rainer Joswig 的说明安装)。我在下面包含了完整的代码。它可能有点低效,但令我惊讶的是,即使在 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 上几秒钟的加载时间。我不确定这是否相关)。
然后我决定尝试通过运行来创建一个独立的可执行文件(一旦加载了上面的代码):
(ccl:save-application "rpitube" :prepend-kernel t)
并从这样的外壳调用它:
$ ./rpitube {playlistURL}
这使:
Unrecognized non-option arguments: ({playlistURL})
这似乎是一个改进,但我仍然做错了什么。我是否需要通过创建我自己的需要 drakma、cl-html-extract 和 split-sequence 的 asdf 包来替换与 Quicklisp 相关的代码,并使用in-package
等加载它?我之前在另一个项目中创建了自己的包 - 特别是因为我想将我的代码拆分为多个文件 - 它似乎可以工作,但我仍然通过ql:quickload
而不是加载我的包in-package
,因为后者似乎从未工作过(也许我应该将其作为一个单独的问题提出)。在这里,rpitube.lisp 代码非常短,似乎没有必要为它创建一个完整的 quickproject 和包,特别是因为无论如何我希望它是一个独立的可执行文件。
所以:我如何更改脚本(或其调用),以便它可以接受 URL 作为命令行参数,可以非交互方式运行(即不打开 REPL),并且只打印所需的输出到终端 - 以空格分隔的 URL 列表 - 没有任何 Quicklisp 加载信息?