~heckyel/livie

15b2ecf65225449c5156251c616db982daf1150e — Jesús 3 years ago 66cc2e1
Add features: playlist and channel
4 files changed, 703 insertions(+), 76 deletions(-)

M README.md
A livie-channel.el
A livie-playlist.el
M livie.el
M README.md => README.md +23 -16
@@ 17,7 17,9 @@ Livie allows the user to search youtube.com and play the video from `mpv`.
## Installation

``` emacs-lisp
(require 'livie "~/.emacs.d/path/to/livie.el")
(push (concat user-emacs-directory "path/to/livie") load-path)
(let* ((file-name-handler-alist nil))
  (require 'livie))
```

## Installation in emacs-personal


@@ 33,21 35,26 @@ Clone repo:
Open `settings.el` write the following:

``` emacs-lisp
(require 'livie "~/.emacs.d/private/livie/livie.el")
(push (concat user-emacs-directory "private/livie") load-path)
(let* ((file-name-handler-alist nil))
  (require 'livie))
```

## Usage
Just run `M-x livie` and enter a search query. `n`, `p` and `tab`
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>`.
Just run `M-x livie` and enter a search query.
Puts it in `livie-mode`. Some of the ways you can interact
with the buffer are shown below.

| key            | binding                      | description                                           |
|----------------|------------------------------|-------------------------------------------------------|
| <kbd>n</kbd>   | `next-line`                  | Move cursor to next line                              |
| <kbd>p</kbd>   | `previous-line`              | Move cursor to previous line                          |
| <kbd>q</kbd>   | `livie-quit`                 | Bury the `*livie*` buffer                             |
| <kbd>s</kbd>   | `livie-search`               | Make a new search                                     |
| <kbd>></kbd>   | `livie-search-next-page`     | Go to next page                                       |
| <kbd><</kbd>   | `livie-search-previous-page` | Go to previous page                                   |
| <kbd>t</kbd>   | `livie-search-type`          | Change the type of results (videos, playlists, etc.). |
| <kbd>S</kbd>   | `livie-sort-videos`          | Sort videos on the current buffer.                    |
| <kbd>Y</kbd>   | `livie-yank-channel-feed`    | Copy the channel RSS feed for the current entry       |
| <kbd>RET</kbd> | `livie-open-entry`           | Open entry                                            |
| <key>y</key>   | `livie-watch-this-video`     | Play video                                            |

A livie-channel.el => livie-channel.el +146 -0
@@ 0,0 1,146 @@
;;; livie-channel.el --- Auxiliary major mode for livie -*- lexical-binding: t; -*-

;; Copyright (C) 2018 - 2021

;;; Authors:

;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info>
;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
;; Pablo BC <pablo.barraza@protonmail.com>

;;; Commentary:

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

;;; Code:

(defcustom livie-channel-sort-criterion "newest"
  "Sort videos by 'newest', 'oldest', or 'popular', as used by `livie-channel-search'."
  :type 'string
  :options '("newest" "oldest" "popular")
  :group 'livie-channel)

(defvar livie-channel-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map text-mode-map)
    (define-key map "h" #'describe-mode)
    (define-key map "q" #'livie--quit-channel-buffer)
    (define-key map ">" #'livie-channel-next-page)
    (define-key map "<" #'livie-channel-previous-page)
    (define-key map (kbd "<tab>") #'next-line)
    (define-key map (kbd "<backtab>") #'previous-line)
    (define-key map "S" #'livie-channel-sort-videos)
    (define-key map (kbd "RET") #'livie-open-entry)
    (define-key map "y" #'livie-watch-this-video)
    map)
  "Keymap for `livie-channel-mode'.")

(define-derived-mode livie-channel-mode livie-mode
  "livie-channel-mode"
  "Mode for displaying livie-channel-videos.
\\{livie-channel-mode-map}"
  (buffer-disable-undo)
  (make-local-variable 'livie-videos)
  (make-local-variable 'livie-channel-author)
  (setq-local livie-type-of-results "video")
  (setf	buffer-read-only t))

(defun livie--channel-query (uid n sort)
  "Query youtube for UID videos, return the Nth page of results, sorted bv SORT."
  (let ((videos (livie--API-call (concat "channels/videos/" uid)
				`(("page" ,n)
				  ("sort_by" ,sort)
				  ("fields" ,livie-default-video-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))

(defun livie-channel ()
  "Open a buffer for the channel of the current entry."
  (let* ((entry (livie-get-current-video))
	 (author (funcall (livie--get-author-function entry) entry))
	 (authorId (funcall (livie--get-authorId-function entry) entry)))
    (get-buffer-create author)
    (switch-to-buffer author)
    (unless (eq major-mode 'livie-channel-mode)
      (livie-channel-mode))
    (setf livie-channel-author author)
    (setf livie-search-term authorId)
    (livie-channel-get-videos authorId)))

(defun livie-channel-get-videos (authorId)
  "Fetch videos from AUTHORID."
  (setf livie-current-page 1)
  (setf livie-videos (livie--channel-query authorId livie-current-page livie-channel-sort-criterion))
  (livie--draw-channel-buffer))

(defun livie-channel-next-page ()
  "Fetch videos from AUTHORID."
  (interactive)
  (setf livie-current-page (1+ livie-current-page))
  (setf livie-videos (livie--channel-query livie-search-term livie-current-page livie-channel-sort-criterion))
  (livie--draw-channel-buffer))

(defun livie-channel-previous-page ()
  "Fetch videos from AUTHORID."
  (interactive)
  (when (> livie-current-page 1)
    (setf livie-current-page (1- livie-current-page))
    (setf livie-videos (livie--channel-query livie-search-term livie-current-page livie-channel-sort-criterion))
    (livie--draw-channel-buffer)))

(defun livie-channel-sort-videos ()
  "Sort videos from the current channel, either by newest (default), oldest, or popular."
  (interactive)
  (setf livie-channel-sort-criterion (completing-read "Sort videos by (default value is newest): " (get 'livie-channel-sort-criterion 'custom-options)))
  (setf livie-current-page 1)
  (setf livie-videos (livie--channel-query livie-search-term livie-current-page livie-channel-sort-criterion))
  (livie--draw-channel-buffer))

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

(defun livie--draw-channel-buffer ()
  "Draws the livie channel buffer i.e. clear everything and write down all videos in `livie-videos'."
  (let ((inhibit-read-only t)
	(current-line (line-number-at-pos)))
    (erase-buffer)
    (setq header-line-format (concat "Displaying videos from " (propertize livie-channel-author 'face 'livie-parameter-face)
				     ", page "
				     (propertize (number-to-string livie-current-page) 'face 'livie-parameter-face)
				     ", sorted by: "
				     (propertize livie-channel-sort-criterion 'face 'livie-parameter-face)))
    (seq-do (lambda (v)
	      (livie--insert-channel-video v)
	      (insert "\n"))
	    livie-videos)
    (goto-char (point-min))))

(defun livie--quit-channel-buffer ()
  "Deletes the current buffer."
  (interactive)
  (kill-buffer (current-buffer)))

(provide 'livie-channel)

;; Local Variables:
;; byte-compile-warnings: (not free-vars)
;; End:
;;; livie-channel.el ends here

A livie-playlist.el => livie-playlist.el +119 -0
@@ 0,0 1,119 @@
;;; livie-playlist.el --- Auxiliary mode to display playlist results

;; Copyright (C) 2018 - 2021

;;; Authors:

;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info>
;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
;; Pablo BC <pablo.barraza@protonmail.com>

;;; Commentary:

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

;;; Code:

(defvar livie-playlist-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map text-mode-map)
    (define-key map "h" #'describe-mode)
    (define-key map "q" #'livie--quit-playlist-buffer)
    (define-key map ">" #'livie-playlist-next-page)
    (define-key map "<" #'livie-playlist-previous-page)
    (define-key map (kbd "<tab>") #'next-line)
    (define-key map (kbd "<backtab>") #'previous-line)
    (define-key map "A" #'livie--open-channel)
    (define-key map (kbd "RET") #'livie-open-entry)
    (define-key map "y" #'livie-watch-this-video)
    map)
  "Keymap for `livie-playlist-mode'.")

(define-derived-mode livie-playlist-mode livie-mode
  "livie-playlist-mode"
  "Mode for displaying livie-playlists.
\\{livie-playlist-mode-map}"
  (buffer-disable-undo)
  (make-local-variable 'livie-videos)
  (make-local-variable 'livie-playlist-title)
  (make-local-variable 'livie-playlistId)
  (setq-local livie-type-of-results "video")
  (setf buffer-read-only t))

(defun livie--process-playlist-videos (videos)
  "Process VIDEOS fetched from a playlist."
  (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)))))
  videos)

(defun livie-playlist--insert-entry (video)
  "Insert VIDEO into the playlist buffer."
  (insert (livie--format-author (livie-video-author video))
	  " "
	  (livie--format-video-length (livie-video-length video))
	  " "
	  (livie--format-title (livie-video-title video))))

(defun livie--get-playlist-videos ()
  "Fetch the videos of the current playlist."
  (let* ((entry (livie-get-current-video)))
    (if (livie-playlist-p entry)
	(progn
	  (switch-to-buffer (livie-playlist-title entry))
	  (unless (eq major-mode 'livie-playlist-mode)
	    (livie-playlist-mode))
	  (setf livie-playlistId (livie-playlist-playlistId entry))
	  (setf livie-playlist-title (livie-playlist-title entry))
	  (livie-playlist--query livie-playlistId livie-current-page)
	  (livie-playlist--draw-buffer)))))

(defun livie-playlist-previous-page ()
  "Go to the previous page of playlist."
  (interactive)
  (setf livie-current-page (1- livie-current-page))
  (livie-playlist--query livie-playlistId livie-current-page)
  (livie-playlist--draw-buffer))

(defun livie-playlist-next-page ()
  "Go to the next page of playlist."
  (interactive)
  (setf livie-current-page (1+ livie-current-page))
  (livie-playlist--query livie-playlistId livie-current-page)
  (livie-playlist--draw-buffer))

(defun livie-playlist--draw-buffer ()
  "Draw buffer for the current playlist."
  (let ((inhibit-read-only t))
    (erase-buffer)
    (setq header-line-format (concat "Displaying videos from " (propertize livie-playlist-title 'face 'livie-parameter-face)
				     ", page "
				     (propertize (number-to-string livie-current-page) 'face 'livie-parameter-face)))
    (seq-do (lambda (v)
	      (livie-playlist--insert-entry v)
	      (insert "\n"))
	    livie-videos)
    (goto-char (point-min))))

(defun livie-playlist--query (playlistID page)
  "Query Invidious for videos from PLAYLISTID on PAGE."
  (let* ((results (livie--API-call (concat "playlists/" livie-playlistId) '(("fields" "videos")
									  ("page" ,livie-current-page)))))
    (setf livie-videos (livie--process-playlist-videos (assoc-default 'videos results)))))

(defun livie--quit-playlist-buffer ()
  "Deletes the current buffer."
  (interactive)
  (kill-buffer (current-buffer)))


(provide 'livie-playlist)

;;; livie-playlist.el ends here

M livie.el => livie.el +415 -60
@@ 7,17 7,22 @@
;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info>
;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
;; Pablo BC <pablo.barraza@protonmail.com>

;;; Commentary:

;; 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 'cl-lib)
(require 'json)
(require 'seq)

(declare-function livie-channel 'livie-channel)
(declare-function livie--get-playlist-videos 'livie-playlist)

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


@@ 29,18 34,70 @@
  :options '(relevance rating upload_date view_count)
  :group 'livie)

(defcustom livie-type-of-results "video"
  "Set what type of results to get when making a search."
  :type 'string
  :options '("video" "playlist" "channel" "all")
  :group 'livie)

(defcustom livie-show-fancy-icons nil
  "If t, enable showing fancy icons in the search buffer."
  :type 'boolean
  :group 'livie)

;; TODO: Try to add support using all-the-icons, or add images instead.
(defcustom livie-icons '((video "Video" "✇")
                         (playlist "Playlist" "🎞")
                         ;; Added a space to this icon so everything is aligned
                         (channel "Channel" "📺 ")
                         (length "" "⌚:")
                         (views "views" "👁")
                         (subCount "subscribers" "🅯")
                         (videoCount "videos" "▶"))
  "Icons for displaying items in buffer.  First string is inserted if `livie-show-fancy-icons' is disabled."
  :type '(alist :value-type (group string string))
  :group 'livie)

(defvar livie--insert-functions '((video . livie--insert-video)
                                 (playlist . livie--insert-playlist)
                                 (channel . livie--insert-channel)))

(defvar livie--default-action-functions '((video . livie--default-video-action)
                                         (playlist . livie--default-playlist-action)
                                         (channel . livie--default-channel-action))
  "Functions to call on an entry.  To modify an action, set the appropiate variable instead.")

(defvar livie--default-video-action #'(lambda ()
                                       (message (livie-video-title (livie-get-current-video))))
  "Action to open a video.  By default it just prints the title to the minibuffer.")

(defvar livie--default-playlist-action #'livie--open-playlist
  "Action to open a playlist.")

(defvar livie--default-channel-action #'livie--open-channel
  "Action to open a channel.")


(defvar livie-invidious-api-url "https://invidious.048596.xyz"
  "URL to Invidious instance.")

(defvar livie-invidious-default-query-fields "author,lengthSeconds,title,videoId,authorId,viewCount,published"
(defvar livie-default-video-query-fields "type,author,lengthSeconds,title,videoId,authorId,viewCount,published"
  "Default fields of interest for video search.")

(defvar livie-default-channel-query-fields "type,author,authorId,subCount,videoCount"
  "Default fields of interest for channel search.")

(defvar livie-default-playlist-query-fields "type,title,playlistId,author,authorId,videoCount"
  "Default fields of interest for playlist search.")

(defvar livie-videos '()
  "List of videos currently on display.")

(defvar livie-published-date-time-string "%Y-%m-%d"
(defcustom 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.")
See `format-time-string' for information on how to edit this variable."
  :type 'string
  :group 'livie)

(defvar-local livie-current-page 1
  "Current page of the current `livie-search-term'")


@@ 48,15 105,33 @@ See `format-time-string' for information on how to edit this variable.")
(defvar-local livie-search-term ""
  "Current search string as used by `livie-search'")

(defvar livie-author-name-reserved-space 20
(defcustom 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).")
too long)."
  :type 'integer
  :group 'livie)

(defvar livie-title-video-reserved-space 100
(defcustom 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).")
too long)."
  :type 'integer
  :group 'livie)

(defcustom livie-title-playlist-reserved-space 30
  "Number of characters reserved for the playlist title in the *livie* buffer.
Note that there will always 3 extra spaces for eventual dots (for names that are
too long)."
  :type 'integer
  :group 'livie)

(defcustom livie-name-channel-reserved-space 50
  "Number of characters reserved for the channel name in the *livie* buffer.
Note that there will always 3 extra spaces for eventual dots (for names that are
too long)."
  :type 'integer
  :group 'livie)

(defface livie-video-published-face
  '((((class color) (background light)) (:foreground "#00C853"))


@@ 78,6 153,23 @@ too long).")
    (((class color) (background dark))  (:foreground "#00BFA5")))
  "Face used for the video views.")

(defface livie-video-title-face
  '((((class color) (background light)) (:foreground "#000000"))
    (((class color) (background dark))  (:foreground "#FFFFFF")))
  "Face used for the video title.")

(defface livie-item-videoCount-face
  '((t :inherit livie-video-view-face))
  "Face used for the videoCount of an entry.")

(defface livie-item-subCount-face
  '((t :inherit livie-video-published-face))
  "Face used for the subCount of an entry.")

(defface livie-parameter-face
  '((t :inherit livie-video-published-face))
  "Face used for the parameters of the current search.")

(defvar livie-mode-map
  (let ((map (make-sparse-keymap)))
    (suppress-keymap map)


@@ 90,12 182,22 @@ too long).")
    (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)
    (define-key map "t" #'livie-search-type)
    (define-key map "S" #'livie-sort-videos)
    (define-key map "C" #'livie-show-channels)
    (define-key map "P" #'livie-show-playlists)
    (define-key map "V" #'livie-show-videos)
    (define-key map "Y" #'livie-yank-channel-feed)
    (define-key map "A" #'livie--open-channel)
    (define-key map (kbd "RET") #'livie-open-entry)
    (define-key map "y" #'livie-watch-this-video)
    map)
  "Keymap for `livie-mode'.")

(define-derived-mode livie-mode text-mode
  "livie-mode"
  "A major mode to query Youtube content through Invidious."
  :group 'livie
  (setq buffer-read-only t)
  (buffer-disable-undo)
  (make-local-variable 'livie-videos))


@@ 113,7 215,7 @@ too long).")
                               (concat name
                                       (make-string (abs extra-chars) ?\ )
                                       "   ")
                             (concat (seq-subseq name 0 livie-author-name-reserved-space)
                             (concat (truncate-string-to-width name livie-author-name-reserved-space)
                                     "..."))))
    (propertize formatted-string 'face 'livie-channel-name-face)))



@@ 125,13 227,38 @@ too long).")
                               (concat title
                                       (make-string (abs extra-chars) ?\ )
                                       "   ")
                             (concat (seq-subseq title 0 livie-title-video-reserved-space)
                             (concat (truncate-string-to-width title livie-title-video-reserved-space)
                                     "..."))))
    (propertize formatted-string 'face 'livie-video-title-face)))

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

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

(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)
  (let ((formatted-string (concat (livie--get-icon 'length)
                                  (format-seconds "%.2h" seconds)
                                  ":"
                                  (format-seconds "%.2m" (mod seconds 3600))
                                  ":"


@@ 140,52 267,123 @@ too long).")

(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))
  (propertize (format "[%s: %d]" (livie--get-icon 'views) views) 'face 'livie-video-view-face))

(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--format-videoCount (videoCount)
  "Format video VIDEOCOUNT to be inserted in the *livie* buffer."
  (propertize (format "[%s: %d]" (livie--get-icon 'videoCount) videoCount) 'face 'livie-item-videoCount-face))

(defun livie--format-subCount (subCount)
  "Format video SUBCOUNT to be inserted in the *livie* buffer."
  (propertize (format "%s: %-10d" (livie--get-icon 'subCount) subCount) 'face 'livie-item-subCount-face))

(defun livie--format-type (type)
  "Insert an icon of TYPE into buffer."
  (if livie-show-fancy-icons
      (propertize (format "%-2s: " (livie--get-icon type)) 'face 'livie-video-title-face)
    (propertize (format "%-10s: " (livie--get-icon type)) 'face 'livie-video-title-face)))

(defun livie--get-icon (item)
  "Get the icon for ITEM from `livie-icons'."
  (let* ((getmarks (assoc-default item livie-icons)))
    (if livie-show-fancy-icons
        (when (fboundp 'second)
          (second getmarks))
      (car getmarks))))

(defun livie--insert-entry (entry)
  "Insert an ENTRY of the form according to its type."
  (let* ((type (if (not (equal livie-type-of-results "all"))
                   (intern livie-type-of-results)
                 (cond ((livie-video-p entry) 'video)
                       ((livie-playlist-p entry) 'playlist)
                       ((livie-channel-p entry) 'channel)
                       (t (error "Invalid entry type")))))
         (func (cdr (assoc type livie--insert-functions))))
    (when (equal livie-type-of-results "all")
      (insert (livie--format-type type)))
    (funcall func entry)))

(defun livie--insert-video (video)
  "Insert `VIDEO' in the current buffer."
  (insert (livie--format-video-published (livie-video-published 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))))

                                        ;TODO: Format playlist and channel entries in buffer
(defun livie--insert-playlist (playlist)
  "Insert PLAYLIST in the current buffer."
  (insert (livie--format-playlist-title (livie-playlist-title playlist))
          " "
          (livie--format-author (livie-video-author video))
          (livie--format-author (livie-playlist-author playlist))
          " "
          (livie--format-video-length (livie-video-length video))
          (livie--format-videoCount (livie-playlist-videoCount playlist))))

(defun livie--insert-channel (channel)
  "Insert CHANNEL in the current buffer."
  (insert (livie--format-channel-name (livie-channel-author channel))
          " "
          (livie--format-title (livie-video-title video))
          (livie--format-subCount (livie-channel-subCount channel))
          " "
          (livie--format-video-views (livie-video-views video))))
          (livie--format-videoCount (livie-channel-videoCount channel))))

(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)
    (setf header-line-format (concat (propertize (capitalize livie-type-of-results) 'face 'livie-parameter-face)
                                     " results for "
                                     (propertize livie-search-term 'face 'livie-parameter-face)
                                     ", page "
                                     (number-to-string livie-current-page)))
                                     (propertize (number-to-string livie-current-page) 'face 'livie-parameter-face)
                                     ", sorted by: "
                                     (propertize (symbol-name livie-sort-criterion) 'face 'livie-parameter-face)))
    (seq-do (lambda (v)
              (livie--insert-video v)
              (livie--insert-entry v)
              (insert "\n"))
            livie-videos)
    (goto-char (point-min))))

(defun livie-enable-fancy-icons ()
  "Enable fancy icons in the *livie* buffer, using `livie-icons'."
  (interactive)
  (setf livie-show-fancy-icons t))

(defun livie-disable-fancy-icons ()
  "Disable fancy icons in the *livie* buffer, using `livie-icons'."
  (interactive)
  (setf livie-show-fancy-icons nil))

(defun livie-toggle-fancy-icons ()
  "Toggle display of fancy-icons in the *livie* buffer, using `livie-icons'."
  (interactive)
  (setf livie-show-fancy-icons (not livie-show-fancy-icons)))

(defun livie-search (query)
  "Search youtube for `QUERY', and redraw the buffer."
  (interactive "sSearch: ")
  (switch-to-buffer "*livie*")
  (setf livie-current-page 1)
  (setf livie-search-term query)
  (setf livie-videos (livie--query query livie-current-page))
  (setf livie-videos (livie--process-results (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)
  (setf livie-videos (livie--query livie-search-term
                                   (1+ livie-current-page)))
  (setf livie-videos (livie--process-results (livie--query livie-search-term
                                                        (1+ livie-current-page))))
  (setf livie-current-page (1+ livie-current-page))
  (livie--draw-buffer))



@@ 193,11 391,63 @@ too long).")
  "Switch to the previous page of the current search.  Redraw the buffer."
  (interactive)
  (when (> livie-current-page 1)
    (setf livie-videos (livie--query livie-search-term
                                     (1- livie-current-page)))
    (setf livie-videos (livie--process-results (livie--query livie-search-term
                                                          (1- livie-current-page))))
    (setf livie-current-page (1- livie-current-page))
    (livie--draw-buffer)))

(defun livie-search-type (&optional arg)
  "Ask for what type of results to display, and search.
If ARG is given, make a new search."
  (interactive "P")
  (when arg
    (setf livie-search-term (read-string "Search: ")))
  (setf livie-current-page 1)
  (setf livie-type-of-results (completing-read "Show: " (get 'livie-type-of-results 'custom-options)))
  (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  (livie--draw-buffer))

(defun livie-show-videos (&optional arg)
  "Show videos for the current search.
If ARG is given, make a new search."
  (interactive "P")
  (when arg
    (setf livie-search-term (read-string "Search: ")))
  (setf livie-current-page 1)
  (setf livie-type-of-results "video")
  (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  (livie--draw-buffer))

(defun livie-show-channels (&optional arg)
  "Show channels for the current search.
If ARG is given, make a new search."
  (interactive "P")
  (when arg
    (setf livie-search-term (read-string "Search: ")))
  (setf livie-current-page 1)
  (setf livie-type-of-results "channel")
  (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  (livie--draw-buffer))

(defun livie-show-playlists (&optional arg)
  "Show playlists for the current search.
If ARG is given, make a new search."
  (interactive "P")
  (when arg
    (setf livie-search-term (read-string "Search: ")))
  (setf livie-current-page 1)
  (setf livie-type-of-results "playlist")
  (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  (livie--draw-buffer))

(defun livie-sort-videos ()
  "Sort videos from the current search from page 1, according to values of `livie-sort-criterion'."
  (interactive)
  (setf livie-sort-criterion (intern (completing-read "Sort videos by (default value is relevance): " (get 'livie-sort-criterion 'custom-options))))
  (setf livie-current-page 1)
  (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  (livie--draw-buffer))

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


@@ 205,14 455,55 @@ too long).")
(defun livie-watch-this-video ()
  "Stream video at point in mpv."
  (interactive)
  (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..."))
  (if (equal (livie--get-entry-type (livie-get-current-video)) '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"))
        (message "Starting streaming..."))
    (message "It's not a video")))

(defun livie-yank-channel-feed (&optional arg)
  "Yank channel's Youtube RSS feed for the current video at point.
If ARG is given, format it as a Invidious RSS feed."
  (interactive "P")
  (let* ((entry (livie-get-current-video))
         (author (funcall (livie--get-author-function entry) entry))
         (authorId (funcall (livie--get-authorId-function entry) entry))
         (url (if arg
                  (concat livie-invidious-api-url "/feed/channel/" authorId)
                (concat "https://www.youtube.com/feeds/videos.xml?channel_id=" authorId))))
    (kill-new url)
    (message "Copied RSS feed for: %s - %s" author url)))

(defun livie--get-entry-type (entry)
  "Return the type of ENTRY."
  (if (not (equal livie-type-of-results "all"))
      (intern livie-type-of-results)
    (cond ((livie-video-p entry) 'video)
          ((livie-playlist-p entry) 'playlist)
          ((livie-channel-p entry) 'channel)
          (t (error "Invalid entry type")))))

(defun livie--get-author-function (entry)
  "Get the author for ENTRY."
  (let* ((type (livie--get-entry-type entry)))
    (pcase type
      ('video #'livie-video-author)
      ('playlist #'livie-playlist-author)
      ('channel #'livie-channel-author)
      (_ (error "Invalid entry type")))))

(defun livie--get-authorId-function (entry)
  "Get the author for ENTRY."
  (let* ((type (livie--get-entry-type entry)))
    (pcase type
      ('video #'livie-video-authorId)
      ('playlist #'livie-playlist-authorId)
      ('channel #'livie-channel-authorId)
      (_ (error "Invalid entry type")))))

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


@@ 240,40 531,104 @@ too long).")
  (views     0  :read-only t)
  (published 0  :read-only t))

;; Maybe type should be part of the struct.
(cl-defstruct (livie-channel (:constructor livie-channel--create)
                            (:copier nil))
  "Information about a Youtube channel."
  (author     "" :read-only t)
  (authorId   "" :read-only t)
  (subCount   0  :read-only t)
  (videoCount 0  :read-only t))

(cl-defstruct (livie-playlist (:constructor livie-playlist--create)
                             (:copier nil))
  "Information about a Youtube playlist."
  (title      "" :read-only t)
  (playlistId "" :read-only t)
  (author     "" :read-only t)
  (authorId   "" :read-only t)
  (videoCount 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)))))
    (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"))
        (error "Curl had problems connecting to Invidious API"))
      (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))
"Query youtube for STRING, return the Nth page of results."
(let ((results (livie--API-call "search" `(("q" ,string)
                                          ("sort_by" ,(symbol-name livie-sort-criterion))
                                          ("type" ,livie-type-of-results)
                                          ("page" ,n)
                                          ("fields" ,(pcase livie-type-of-results
                                                       ("video" livie-default-video-query-fields)
                                                       ("playlist" livie-default-playlist-query-fields)
                                                       ("channel" livie-default-channel-query-fields)
                                                       ;; I mean, it does get the job done... fix later.
                                                       ("all" (concat livie-default-channel-query-fields
                                                                      ","
                                                                      livie-default-playlist-query-fields
                                                                      ","
                                                                      livie-default-video-query-fields))))))))
  results))

(defun livie--process-results (results &optional type)
  "Process RESULTS and turn them into objects, is TYPE is not given, get it from RESULTS."
  (dotimes (i (length results))
    (let* ((v (aref results i))
           (type (or type (assoc-default 'type v))))
      (aset results i  (pcase type
                         ("video" (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)))
                         ("playlist" (livie-playlist--create
                                      :title      (assoc-default 'title v)
                                      :playlistId (assoc-default 'playlistId v)
                                      :author     (assoc-default 'author v)
                                      :authorId   (assoc-default 'authorId v)
                                      :videoCount (assoc-default 'videoCount v)))
                         ("channel" (livie-channel--create
                                     :author     (assoc-default 'author v)
                                     :authorId   (assoc-default 'authorId v)
                                     :subCount   (assoc-default 'subCount v)
                                     :videoCount (assoc-default 'videoCount v)))))))
  results)

(defun livie-open-entry ()
  "Open the entry at point depending on it's type."
  (interactive)
  (let* ((entry (livie-get-current-video))
         (type (livie--get-entry-type entry)))
    (funcall (symbol-value (assoc-default type livie--default-action-functions)))))

(defun livie--open-channel ()
  "Fetch the channel page for the entry at point."
  (interactive)
  (require 'livie-channel)
  (livie-channel))

(defun livie--open-playlist ()
  "Open the contents of the entry at point, if it's a playlist."
  (interactive)
  (require 'livie-playlist)
  (livie--get-playlist-videos))

(provide 'livie)