Add features: playlist and channel

This commit is contained in:
Jesús 2021-01-17 15:07:10 -05:00
parent 66cc2e1e08
commit 15b2ecf652
No known key found for this signature in database
GPG Key ID: F6EE7BC59A315766
4 changed files with 702 additions and 75 deletions

View File

@ -17,7 +17,9 @@ Livie allows the user to search youtube.com and play the video from `mpv`.
## Installation ## Installation
``` emacs-lisp ``` 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 ## Installation in emacs-personal
@ -33,21 +35,26 @@ Clone repo:
Open `settings.el` write the following: Open `settings.el` write the following:
``` emacs-lisp ``` 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 ## Usage
Just run `M-x livie` and enter a search query. `n`, `p` and `tab` Just run `M-x livie` and enter a search query.
can be used to navigate the buffer. Puts it in `livie-mode`. Some of the ways you can interact
with the buffer are shown below.
| key | binding | | key | binding | description |
|-------------------|------------------------------| |----------------|------------------------------|-------------------------------------------------------|
| <key>n</key> | `next-line` | | <kbd>n</kbd> | `next-line` | Move cursor to next line |
| <key>p</key> | `previous-line` | | <kbd>p</kbd> | `previous-line` | Move cursor to previous line |
| <key>q</key> | `livie-quit` | | <kbd>q</kbd> | `livie-quit` | Bury the `*livie*` buffer |
| <key>s</key> | `livie-search` | | <kbd>s</kbd> | `livie-search` | Make a new search |
| <key>></key> | `livie-search-next-page` | | <kbd>></kbd> | `livie-search-next-page` | Go to next page |
| <key><</key> | `livie-search-previous-page` | | <kbd><</kbd> | `livie-search-previous-page` | Go to previous page |
| <key>return</key> | `livie-watch-this-video` | | <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. |
Type `s` to enter another search. To watch a video, press `<enter>`. | <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 |

146
livie-channel.el Normal file
View File

@ -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

119
livie-playlist.el Normal file
View File

@ -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

475
livie.el
View File

@ -7,17 +7,22 @@
;; Charlie Ritter <chewzerita@posteo.net> ;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info> ;; Jesus E. <heckyel@hyperbola.info>
;; Gabriele Rastello <gabriele.rastello@edu.unito.it> ;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
;; Pablo BC <pablo.barraza@protonmail.com>
;;; Commentary: ;;; Commentary:
;; livie grabs a list of youtube videos based on a search. ;; livie grabs a list of youtube videos based on a search.
;; the user can then select a video to watch through `livie-player' ;; the user can then select a video to watch through `livie-player'
;;; Code: ;;; Code:
(require 'cl-lib) (require 'cl-lib)
(require 'json) (require 'json)
(require 'seq) (require 'seq)
(declare-function livie-channel 'livie-channel)
(declare-function livie--get-playlist-videos 'livie-playlist)
(defgroup livie '() (defgroup livie '()
"Livie is Video in Emacs" "Livie is Video in Emacs"
:prefix "livie-" :prefix "livie-"
@ -29,18 +34,70 @@
:options '(relevance rating upload_date view_count) :options '(relevance rating upload_date view_count)
:group 'livie) :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" (defvar livie-invidious-api-url "https://invidious.048596.xyz"
"URL to Invidious instance.") "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.") "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 '() (defvar livie-videos '()
"List of videos currently on display.") "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. "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 (defvar-local livie-current-page 1
"Current page of the current `livie-search-term'") "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 "" (defvar-local livie-search-term ""
"Current search string as used by `livie-search'") "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. "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 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. "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 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 (defface livie-video-published-face
'((((class color) (background light)) (:foreground "#00C853")) '((((class color) (background light)) (:foreground "#00C853"))
@ -78,6 +153,23 @@ too long).")
(((class color) (background dark)) (:foreground "#00BFA5"))) (((class color) (background dark)) (:foreground "#00BFA5")))
"Face used for the video views.") "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 (defvar livie-mode-map
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(suppress-keymap map) (suppress-keymap map)
@ -90,12 +182,22 @@ too long).")
(define-key map "s" #'livie-search) (define-key map "s" #'livie-search)
(define-key map ">" #'livie-search-next-page) (define-key map ">" #'livie-search-next-page)
(define-key map "<" #'livie-search-previous-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) map)
"Keymap for `livie-mode'.") "Keymap for `livie-mode'.")
(define-derived-mode livie-mode text-mode (define-derived-mode livie-mode text-mode
"livie-mode" "livie-mode"
"A major mode to query Youtube content through Invidious."
:group 'livie
(setq buffer-read-only t) (setq buffer-read-only t)
(buffer-disable-undo) (buffer-disable-undo)
(make-local-variable 'livie-videos)) (make-local-variable 'livie-videos))
@ -113,7 +215,7 @@ too long).")
(concat name (concat name
(make-string (abs extra-chars) ?\ ) (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))) (propertize formatted-string 'face 'livie-channel-name-face)))
@ -125,13 +227,38 @@ too long).")
(concat title (concat title
(make-string (abs extra-chars) ?\ ) (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)
"...")))) "..."))))
formatted-string)) (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)
"..."))))
(propertize formatted-string 'face 'livie-channel-name-face)))
(defun livie--format-video-length (seconds) (defun livie--format-video-length (seconds)
"Given an amount of SECONDS, format it nicely to be inserted in the *livie* buffer." "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)) (format-seconds "%.2m" (mod seconds 3600))
":" ":"
@ -140,52 +267,123 @@ too long).")
(defun livie--format-video-views (views) (defun livie--format-video-views (views)
"Format video VIEWS to be inserted in the *livie* buffer." "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) (defun livie--format-video-published (published)
"Format video PUBLISHED date to be inserted in the *livie* buffer." "Format video PUBLISHED date to be inserted in the *livie* buffer."
(propertize (format-time-string livie-published-date-time-string (seconds-to-time published)) (propertize (format-time-string livie-published-date-time-string (seconds-to-time published))
'face 'livie-video-published-face)) '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) (defun livie--insert-video (video)
"Insert `VIDEO' in the current buffer." "Insert VIDEO in the current buffer."
(insert (livie--format-video-published (livie-video-published video)) (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 () (defun livie--draw-buffer ()
"Draws the livie buffer i.e. clear everything and write down all videos in `livie-videos'." "Draws the livie buffer i.e. clear everything and write down all videos in `livie-videos'."
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
(setf header-line-format (concat "Search results for " (setf header-line-format (concat (propertize (capitalize livie-type-of-results) 'face 'livie-parameter-face)
(propertize livie-search-term 'face 'livie-video-published-face) " results for "
(propertize livie-search-term 'face 'livie-parameter-face)
", page " ", 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) (seq-do (lambda (v)
(livie--insert-video v) (livie--insert-entry v)
(insert "\n")) (insert "\n"))
livie-videos) livie-videos)
(goto-char (point-min)))) (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) (defun livie-search (query)
"Search youtube for `QUERY', and redraw the buffer." "Search youtube for `QUERY', and redraw the buffer."
(interactive "sSearch: ") (interactive "sSearch: ")
(switch-to-buffer "*livie*")
(setf livie-current-page 1) (setf livie-current-page 1)
(setf livie-search-term query) (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)) (livie--draw-buffer))
(defun livie-search-next-page () (defun livie-search-next-page ()
"Switch to the next page of the current search. Redraw the buffer." "Switch to the next page of the current search. Redraw the buffer."
(interactive) (interactive)
(setf livie-videos (livie--query livie-search-term (setf livie-videos (livie--process-results (livie--query livie-search-term
(1+ livie-current-page))) (1+ livie-current-page))))
(setf livie-current-page (1+ livie-current-page)) (setf livie-current-page (1+ livie-current-page))
(livie--draw-buffer)) (livie--draw-buffer))
@ -193,11 +391,63 @@ too long).")
"Switch to the previous page of the current search. Redraw the buffer." "Switch to the previous page of the current search. Redraw the buffer."
(interactive) (interactive)
(when (> livie-current-page 1) (when (> livie-current-page 1)
(setf livie-videos (livie--query livie-search-term (setf livie-videos (livie--process-results (livie--query livie-search-term
(1- livie-current-page))) (1- livie-current-page))))
(setf livie-current-page (1- livie-current-page)) (setf livie-current-page (1- livie-current-page))
(livie--draw-buffer))) (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 () (defun livie-get-current-video ()
"Get the currently selected video." "Get the currently selected video."
(aref livie-videos (1- (line-number-at-pos)))) (aref livie-videos (1- (line-number-at-pos))))
@ -205,14 +455,55 @@ too long).")
(defun livie-watch-this-video () (defun livie-watch-this-video ()
"Stream video at point in mpv." "Stream video at point in mpv."
(interactive) (interactive)
(let* ((video (livie-get-current-video)) (if (equal (livie--get-entry-type (livie-get-current-video)) 'video)
(id (livie-video-id video))) (let* ((video (livie-get-current-video))
(start-process "livie mpv" nil (id (livie-video-id video)))
"mpv" (start-process "livie mpv" nil
(concat "https://www.youtube.com/watch?v=" id)) "mpv"
"--ytdl-format=bestvideo[height<=?720]+bestaudio/best") (concat "https://www.youtube.com/watch?v=" id
(delete-other-windows) "--ytdl-format=bestvideo[height<=?720]+bestaudio/best"))
(message "Starting streaming...")) (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 () (defun livie-buffer ()
"Name for the main livie buffer." "Name for the main livie buffer."
@ -240,40 +531,104 @@ too long).")
(views 0 :read-only t) (views 0 :read-only t)
(published 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) (defun livie--API-call (method args)
"Perform a call to the invidious API method METHOD passing 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 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." zero exit code otherwise the request body is parsed by `json-read' and returned."
(with-temp-buffer (with-temp-buffer
(let ((exit-code (call-process "curl" nil t nil (let ((exit-code (call-process "curl" nil t nil
"--silent" "--silent"
"-X" "GET" "-X" "GET"
(concat livie-invidious-api-url (concat livie-invidious-api-url
"/api/v1/" method "/api/v1/" method
"?" (url-build-query-string args))))) "?" (url-build-query-string args)))))
(unless (= exit-code 0) (unless (= exit-code 0)
(error "Curl had problems connecting to Invidious")) (error "Curl had problems connecting to Invidious API"))
(goto-char (point-min)) (goto-char (point-min))
(json-read)))) (json-read))))
(defun livie--query (string n) (defun livie--query (string n)
"Query youtube for STRING, return the Nth page of results." "Query youtube for STRING, return the Nth page of results."
(let ((videos (livie--API-call "search" `(("q", string) (let ((results (livie--API-call "search" `(("q" ,string)
("sort_by", (symbol-name livie-sort-criterion)) ("sort_by" ,(symbol-name livie-sort-criterion))
("page", n) ("type" ,livie-type-of-results)
("fields", livie-invidious-default-query-fields))))) ("page" ,n)
(dotimes (i (length videos)) ("fields" ,(pcase livie-type-of-results
(let ((v (aref videos i))) ("video" livie-default-video-query-fields)
(aset videos i ("playlist" livie-default-playlist-query-fields)
(livie-video--create ("channel" livie-default-channel-query-fields)
:title (assoc-default 'title v) ;; I mean, it does get the job done... fix later.
:author (assoc-default 'author v) ("all" (concat livie-default-channel-query-fields
:authorId (assoc-default 'authorId v) ","
:length (assoc-default 'lengthSeconds v) livie-default-playlist-query-fields
:id (assoc-default 'videoId v) ","
:views (assoc-default 'viewCount v) livie-default-video-query-fields))))))))
:published (assoc-default 'published v))))) results))
videos))
(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) (provide 'livie)