~heckyel/livie

e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad — Jesús 3 years ago be65320
Remove python dependencie and integrate Invidious API
4 files changed, 257 insertions(+), 165 deletions(-)

M README.md
M livie.el
D livie.py
M screenshot.png
M README.md => README.md +16 -7
@@ 8,12 8,11 @@ Livie allows the user to search youtube.com and play the video from `mpv`.

## Requirements

- `python >= 3.5`
- `python-requests`
- `hypervideo`
- `curl`
- `mpv`

  `sudo pacman -S python mpv python-requests hypervideo`
  `sudo pacman -S mpv hypervideo`

## Installation



@@ 30,7 29,7 @@ Create new dir:

Clone repo:

    git clone https://libregit.org/heckyel/livie.git ~/.emacs.d/private/livie
    git clone https://git.sr.ht/~heckyel/livie ~/.emacs.d/private/livie

Open `settings.el` write the following:



@@ 40,7 39,17 @@ Open `settings.el` write the following:
```

## Usage

Just run `M-x livie` and enter a search query. `n`, `p` and `tab`
can be used to navigate the buffer.  Type `s` to enter another search.
To watch a video, press `<enter>`.
can be used to navigate the buffer.

| key               | binding                      |
|-------------------|------------------------------|
| <key>n</key>      | `next-line`                  |
| <key>p</key>      | `previous-line`              |
| <key>q</key>      | `livie-quit`                 |
| <key>s</key>      | `livie-search`               |
| <key>></key>      | `livie-search-next-page`     |
| <key><</key>      | `livie-search-previous-page` |
| <key>return</key> | `livie-watch-this-video`     |

Type `s` to enter another search. To watch a video, press `<enter>`.

M livie.el => livie.el +241 -117
@@ 1,155 1,279 @@
;;; livie.el --- Livie is Video in Emacs -*- lexical-binding: t; -*-

;; Copyright (C) 2018
;; Copyright (C) 2018 - 2021

;;; Authors:

;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info>
;; Gabriele Rastello <gabriele.rastello@edu.unito.it>

;;; Commentary:

;; livie grabs a list of youtube videos based on a search.  the user
;; can then select a video to watch through `livie-player'

;; livie grabs a list of youtube videos based on a search.
;; the user can then select a video to watch through `livie-player'
;;; Code:

(require 'f)
(require 'cl-lib)
(require 'json)
(require 'seq)

(defgroup livie '()
  "Livie is Video in Emacs"
  :prefix "livie-"
  :group 'livie)

(defcustom livie-player "mpv"
  "Default video player to use with livie."
  :group 'livie
  :type 'string)
(defcustom livie-sort-criterion 'relevance
  "Criterion to sort the results of the search query."
  :type 'symbol
  :options '(relevance rating upload_date view_count)
  :group 'livie)

(defcustom livie-player-args '()
  "Command line arguments for `livie-player'."
  :group 'livie
  :type '(list)
  :tag "Livie Player Arguments")
(defvar livie-invidious-api-url "https://invidious.048596.xyz"
  "URL to Invidious instance.")

(defcustom livie-python-name "python3"
  "Name of the python executable."
  :group 'livie
  :type 'string)
(defvar livie-invidious-default-query-fields "author,lengthSeconds,title,videoId,authorId,viewCount,published"
  "Default fields of interest for video search.")

(defcustom livie-script-path nil
  "Full path of livie.py."
  :group 'livie
  :type 'file)
(defvar livie-videos '()
  "List of videos currently on display.")

(defcustom livie-buffer-name "*livie*"
  "Name of the buffer to show results."
  :group 'livie
  :type 'string)
(defvar livie-published-date-time-string "%Y-%m-%d"
  "Time-string used to render the published date of the video.
See `format-time-string' for information on how to edit this variable.")

(defvar livie-youtube-regexp "https://www.youtube.com/watch\\?v=[A-Za-z0-9_\\-]\\{11\\}")
(defvar-local livie-current-page 1
  "Current page of the current `livie-search-term'")

(define-derived-mode livie-mode
  special-mode "livie"
  "Major mode for livie.")
(defvar-local livie-search-term ""
  "Current search string as used by `livie-search'")

(defun livie-close-window ()
  "Close the livie window and bury the buffer."
  (interactive)
  (bury-buffer)
  (delete-window)
  (message nil))
(defvar livie-author-name-reserved-space 20
  "Number of characters reserved for the channel's name in the *livie* buffer.
Note that there will always 3 extra spaces for eventual dots (for names that are
too long).")

(defun livie-next-video ()
  "Goto the next video in the buffer."
  (interactive)
  (forward-line 1)
  (search-forward-regexp livie-youtube-regexp)
  (livie-prev-video))
(defvar livie-title-video-reserved-space 100
  "Number of characters reserved for the video title in the *livie* buffer.
Note that there will always 3 extra spaces for eventual dots (for names that are
too long).")

(defface livie-video-published-face
  '((((class color) (background light)) (:foreground "#00C853"))
    (((class color) (background dark))  (:foreground "#00E676")))
  "Face used for the video published date.")

(defface livie-channel-name-face
  '((((class color) (background light)) (:foreground "#FFC400"))
    (((class color) (background dark))  (:foreground "#FFFF00")))
  "Face used for channel names.")

(defface livie-video-length-face
  '((((class color) (background light)) (:foreground "#6A1B9A"))
    (((class color) (background dark))  (:foreground "#AA00FF")))
  "Face used for the video length.")

(defun livie-prev-video ()
  "Goto the previous video in the buffer."
(defface livie-video-view-face
  '((((class color) (background light)) (:foreground "#00695C"))
    (((class color) (background dark))  (:foreground "#00BFA5")))
  "Face used for the video views.")

(defvar livie-mode-map
  (let ((map (make-sparse-keymap)))
    (suppress-keymap map)
    (define-key map "q" #'livie-quit)
    (define-key map "h" #'describe-mode)
    (define-key map "n" #'next-line)
    (define-key map "p" #'previous-line)
    (define-key map (kbd "<tab>") #'next-line)
    (define-key map (kbd "<backtab>") #'previous-line)
    (define-key map "s" #'livie-search)
    (define-key map ">" #'livie-search-next-page)
    (define-key map "<" #'livie-search-previous-page)
    (define-key map (kbd "<return>") 'livie-watch-this-video)
    map)
  "Keymap for `livie-mode'.")

(define-derived-mode livie-mode text-mode
  "livie-mode"
  (setq buffer-read-only t)
  (buffer-disable-undo)
  (make-local-variable 'livie-videos))

(defun livie-quit ()
  "Quit livie buffer."
  (interactive)
  (search-backward-regexp livie-youtube-regexp))
  (quit-window))

(defun livie--format-author (name)
  "Format a channel NAME to be inserted in the *livie* buffer."
  (let* ((n (string-width name))
         (extra-chars (- n livie-author-name-reserved-space))
         (formatted-string (if (<= extra-chars 0)
                               (concat name
                                       (make-string (abs extra-chars) ?\ )
                                       "   ")
                             (concat (seq-subseq name 0 livie-author-name-reserved-space)
                                     "..."))))
    (propertize formatted-string 'face 'livie-channel-name-face)))

(defun livie--format-title (title)
  "Format a video TITLE to be inserted in the *livie* buffer."
  (let* ((n (string-width title))
         (extra-chars (- n livie-title-video-reserved-space))
         (formatted-string (if (<= extra-chars 0)
                               (concat title
                                       (make-string (abs extra-chars) ?\ )
                                       "   ")
                             (concat (seq-subseq title 0 livie-title-video-reserved-space)
                                     "..."))))
    formatted-string))

(defun livie--format-video-length (seconds)
  "Given an amount of SECONDS, format it nicely to be inserted in the *livie* buffer."
  (let ((formatted-string (concat (format-seconds "%.2h" seconds)
                                  ":"
                                  (format-seconds "%.2m" (mod seconds 3600))
                                  ":"
                                  (format-seconds "%.2s" (mod seconds 60)))))
    (propertize formatted-string 'face 'livie-video-length-face)))

(defun livie--format-video-views (views)
  "Format video VIEWS to be inserted in the *livie* buffer."
  (propertize (concat "[views:" (number-to-string views) "]") 'face 'livie-video-view-face))

(defun livie-this-video ()
  "Go to the start of the current video."
(defun livie--format-video-published (published)
  "Format video PUBLISHED date to be inserted in the *livie* buffer."
  (propertize (format-time-string livie-published-date-time-string (seconds-to-time published))
              'face 'livie-video-published-face))

(defun livie--insert-video (video)
  "Insert `VIDEO' in the current buffer."
  (insert (livie--format-video-published (livie-video-published video))
          " "
          (livie--format-author (livie-video-author video))
          " "
          (livie--format-video-length (livie-video-length video))
          " "
          (livie--format-title (livie-video-title video))
          " "
          (livie--format-video-views (livie-video-views video))))

(defun livie--draw-buffer ()
  "Draws the livie buffer i.e. clear everything and write down all videos in `livie-videos'."
  (let ((inhibit-read-only t))
    (erase-buffer)
    (setf header-line-format (concat "Search results for "
                                     (propertize livie-search-term 'face 'livie-video-published-face)
                                     ", page "
                                     (number-to-string livie-current-page)))
    (seq-do (lambda (v)
              (livie--insert-video v)
              (insert "\n"))
            livie-videos)
    (goto-char (point-min))))

(defun livie-search (query)
  "Search youtube for `QUERY', and redraw the buffer."
  (interactive "sSearch: ")
  (setf livie-current-page 1)
  (setf livie-search-term query)
  (setf livie-videos (livie--query query livie-current-page))
  (livie--draw-buffer))

(defun livie-search-next-page ()
  "Switch to the next page of the current search.  Redraw the buffer."
  (interactive)
  (move-beginning-of-line nil)
  (ignore-errors (forward-line -1))
  (livie-next-video))
  (setf livie-videos (livie--query livie-search-term
                                   (1+ livie-current-page)))
  (setf livie-current-page (1+ livie-current-page))
  (livie--draw-buffer))

(defun livie-copy-video ()
  "Copy the currently selected video into the kill ring."
(defun livie-search-previous-page ()
  "Switch to the previous page of the current search.  Redraw the buffer."
  (interactive)
  (livie-this-video)
  (push-mark)
  (move-end-of-line nil)
  (kill-ring-save nil nil t)
  (livie-this-video))
  (when (> livie-current-page 1)
    (setf livie-videos (livie--query livie-search-term
                                     (1- livie-current-page)))
    (setf livie-current-page (1- livie-current-page))
    (livie--draw-buffer)))

(defun livie-get-current-video ()
  "Get the currently selected video."
  (aref livie-videos (1- (line-number-at-pos))))

(defun livie-watch-this-video ()
  "Watch video under the cursor."
  "Stream video at point in mpv."
  (interactive)
  (livie-copy-video)
  (livie-watch (car kill-ring))
  (delete-other-windows))

(defun livie-watch (url)
  "Watch video at URL using `livie-player' and `livie-player-args'."
  (interactive "sURL: ")
  (apply 'start-process
         (cl-concatenate
          'list
          (list "livie"
                nil
                livie-player)
          livie-player-args
          (list url)))
  (message "Loading video..."))

(defun livie (query)
  "Livie is Video in Emacs.

Livie will prompt the user for a QUERY.  This is fed into a
python script that scrapes youtube.com for search results.  The
results are displayed in an Emacs buffer.

See also: `livie-mode'."
  (interactive "sSearch: ")
  (unless livie-script-path
    (error "Please set `livie-script-path'"))
  (if (equal (buffer-name) livie-buffer-name)
      (progn
        (read-only-mode -1)
        (erase-buffer))
    (progn
      (select-window (split-window))
      (ignore-errors (kill-buffer livie-buffer-name))
      (switch-to-buffer (generate-new-buffer livie-buffer-name))))
  (call-process livie-python-name
                nil t t
                livie-script-path
                query)
  (livie-mode)
  (font-lock-ensure)
  (goto-char (point-min))
  (message (concat "Results for: " query)))

(font-lock-add-keywords
 'livie-mode
 `((,livie-youtube-regexp . 'link)
   ("title: \\(.*\\)" 1 'bold)
   ("channel: \\(.*\\)" 1 'italic)
   ("^ +[a-zA-Z]+:" . 'shadow)))

(define-key livie-mode-map "s" 'livie)
(define-key livie-mode-map "q" 'livie-close-window)
(define-key livie-mode-map (kbd "<tab>") 'livie-next-video)
(define-key livie-mode-map (kbd "<backtab>") 'livie-prev-video)
(define-key livie-mode-map "n" 'livie-next-video)
(define-key livie-mode-map "p" 'livie-prev-video)
(define-key livie-mode-map (kbd "<return>") 'livie-watch-this-video)
  (let* ((video (livie-get-current-video))
         (id    (livie-video-id video)))
    (start-process "livie mpv" nil
                   "mpv"
                   (concat "https://www.youtube.com/watch?v=" id))
    "--ytdl-format=bestvideo[height<=?720]+bestaudio/best")
  (delete-other-windows)
  (message "Starting streaming..."))

(defun livie-buffer ()
  "Name for the main livie buffer."
  (get-buffer-create "*livie*"))

;;;###autoload
(defun livie ()
  "Enter livie."
  (interactive)
  (switch-to-buffer (livie-buffer))
  (unless (eq major-mode 'livie-mode)
    (livie-mode))
  (when (seq-empty-p livie-search-term)
    (call-interactively #'livie-search)))

;; Youtube interface stuff below.
(cl-defstruct (livie-video (:constructor livie-video--create)
                           (:copier nil))
  "Information about a Youtube video."
  (title     "" :read-only t)
  (id        0  :read-only t)
  (author    "" :read-only t)
  (authorId  "" :read-only t)
  (length    0  :read-only t)
  (views     0  :read-only t)
  (published 0 :read-only t))

(defun livie--API-call (method args)
  "Perform a call to the invidious API method METHOD passing ARGS.
Curl is used to perform the request.  An error is thrown if it exits with a non
zero exit code otherwise the request body is parsed by `json-read' and returned."
  (with-temp-buffer
    (let ((exit-code (call-process "curl" nil t nil
                                   "--silent"
                                   "-X" "GET"
                                   (concat livie-invidious-api-url
                                           "/api/v1/" method
                                           "?" (url-build-query-string args)))))
      (unless (= exit-code 0)
        (error "Curl had problems connecting to Invidious"))
      (goto-char (point-min))
      (json-read))))

(defun livie--query (string n)
  "Query youtube for STRING, return the Nth page of results."
  (let ((videos (livie--API-call "search" `(("q", string)
                                            ("sort_by", (symbol-name livie-sort-criterion))
                                            ("page", n)
                                            ("fields", livie-invidious-default-query-fields)))))
    (dotimes (i (length videos))
      (let ((v (aref videos i)))
        (aset videos i
              (livie-video--create
               :title     (assoc-default 'title v)
               :author    (assoc-default 'author v)
               :authorId  (assoc-default 'authorId v)
               :length    (assoc-default 'lengthSeconds v)
               :id        (assoc-default 'videoId v)
               :views     (assoc-default 'viewCount v)
               :published (assoc-default 'published v)))))
    videos))

(provide 'livie)


D livie.py => livie.py +0 -41
@@ 1,41 0,0 @@
"""This module does render video"""

import sys
import json
import requests

URL = 'https://youtube-scrape.herokuapp.com'
INPUT = sys.argv[1]
SEARCH = '%s/api/search?q=%s' % (URL, INPUT)
REQUEST = requests.get(SEARCH)
FIRST = True

data = json.loads(REQUEST.content.decode('utf-8'))
items = data['results']

# with open('output.json', 'w') as json_file:
#     json.dump(items, json_file)

for item in items:
    try:
        title = item['video']['title']
        link = item['video']['url']
        author = item['uploader']['username']
        time = item['video']['duration']
        uploaded = item['video']['upload_date']
        views = item['video']['views']
    except KeyError:
        continue

    if FIRST:
        FIRST = False
    else:
        print()  # print skip line

    # prints
    print('    title: %s' % title)
    print('      url: %s' % link)
    print('  channel: %s' % author)
    print(' uploaded: %s' % uploaded)
    print('     time: %s' % time)
    print('    views: %s' % views)

M screenshot.png => screenshot.png +0 -0