Вызов сценария CCL + Quicklisp как исполняемого с аргументами командной строки и достижение желаемого результата

После обнаружения очень простого способа просмотра видео YouTube из командной строки с помощью моего нового Raspberry Pi 2 (с запущенным Raspbian) с использованием только легко доступных пакетов, а именно:

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

Я сразу же хотел, чтобы таким образом можно было просматривать целые плейлисты YouTube. Так что я видел в этом идеальный повод взломать решение в Common Lisp:)

Мое решение (образно названное RpiTube) - это сценарий, который при задании URL-адреса плейлиста YouTube выполняет поиск в источнике HTML страницы и извлекает URL-адреса для видео, содержащихся в нем. Затем я могу передать эти URL-адреса скрипту Bash, который в конечном итоге вызывает указанную выше команду для каждого видео отдельно, одно за другим. Сам сценарий Common Lisp завершен и работает, однако мне трудно вызывать его с URL-адресом в качестве аргумента командной строки. Это в основном потому, что я все еще довольно новичок в Quicklisp, пакетах Lisp и создании исполняемых файлов из кода Common Lisp.

Я использую Clozure Common Lisp (CCL) с Quicklisp (установленным в соответствии с инструкциями 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 и индикаторы выполнения выводятся на терминал, чего я тоже не хочу. (Между прочим, как предположил Райнер, я не добавил Quicklisp в мой файл инициализации CCL, поскольку я обычно не хочу дополнительных издержек, т.е. нескольких секунд загрузки на Raspberry Pi. Я не уверен, что это актуально).

Затем я решил попытаться создать автономный исполняемый файл, запустив его (после загрузки приведенного выше кода):

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

И вызывая его из оболочки так:

$ ./rpitube {playlistURL}

который дает:

Unrecognized non-option arguments: ({playlistURL})

что кажется улучшением, но я все еще делаю что-то не так. Нужно ли заменять код, связанный с Quicklisp, путем создания моего собственного asdf-пакета, требующего drakma, cl-html-extract и split-sequence, и загрузки его с in-package, так далее.? Я создал свой собственный пакет ранее в другом проекте - особенно потому, что я хотел разделить мой код на несколько файлов - и это, кажется, работает, но я все еще загружал свой пакет через ql:quickload в отличие от in-package, так как последний никогда не работал (возможно, я должен задать об этом как отдельный вопрос). Здесь код rpitube.lisp настолько короток, что кажется ненужным создавать целый быстрый проект и пакет для него, тем более, что я все равно хочу, чтобы он был автономным исполняемым файлом.

Итак: как мне изменить скрипт (или его вызов), чтобы он мог принимать URL-адрес в качестве аргумента командной строки, запускаться не в интерактивном режиме (то есть не открывать REPL) и выводить ТОЛЬКО желаемый результат в терминал - разделенный пробелами список URL - без какой-либо информации о загрузке Quicklisp?

2 ответа

Хорошо, мне удалось адаптировать решение по предложению, связанному с пользователем @mn выше. Теперь кажется, что 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 звонки; заменить мой собственный ocurrences функция со встроенным count (: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'

Большое спасибо.

Я посмотрел на это некоторые и хотел бы поделиться тем, что я нашел. Существует также несколько библиотек 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, похоже, не имеет эквивалентной опции, но предыдущий ответ - /questions/27051938/kak-zapustit-clozure-cl-lisp-iz-stsenariya-obolochki-na-os-x/27051955#27051955 - предлагает написать короткий сценарий Bash, который, по сути, будет вести себя так, как вы надеялись, CCL с этой попыткой,

quickload вызовы могут быть отключены аргументом:

  (ql:quickload :drakma :silent t)
Другие вопросы по тегам