645 lines
26 KiB
EmacsLisp
645 lines
26 KiB
EmacsLisp
;;; livie.el --- Livie is Video in Emacs -*- 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:
|
|
|
|
(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-"
|
|
:group 'livie)
|
|
|
|
(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-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-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.")
|
|
|
|
(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."
|
|
:type 'string
|
|
:group 'livie)
|
|
|
|
(defvar-local livie-current-page 1
|
|
"Current page of the current `livie-search-term'")
|
|
|
|
(defvar-local livie-search-term ""
|
|
"Current search string as used by `livie-search'")
|
|
|
|
(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)."
|
|
:type 'integer
|
|
:group 'livie)
|
|
|
|
(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)."
|
|
: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 "#1B5E20"))
|
|
(((class color) (background dark)) (:foreground "#00E676")))
|
|
"Face used for the video published date.")
|
|
|
|
(defface livie-channel-name-face
|
|
'((((class color) (background light)) (:foreground "#FF6D00"))
|
|
(((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.")
|
|
|
|
(defface livie-video-view-face
|
|
'((((class color) (background light)) (:foreground "#00695C"))
|
|
(((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)
|
|
(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 "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))
|
|
|
|
(defun livie-quit ()
|
|
"Quit livie buffer."
|
|
(interactive)
|
|
(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 (truncate-string-to-width name 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 (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)
|
|
"..."))))
|
|
(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 (livie--get-icon 'length)
|
|
(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 (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))
|
|
" "
|
|
(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-playlist-author playlist))
|
|
" "
|
|
(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-subCount (livie-channel-subCount channel))
|
|
" "
|
|
(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 (propertize (capitalize livie-type-of-results) 'face 'livie-parameter-face)
|
|
" results for "
|
|
(propertize livie-search-term 'face 'livie-parameter-face)
|
|
", 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-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--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--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-previous-page ()
|
|
"Switch to the previous page of the current search. Redraw the buffer."
|
|
(interactive)
|
|
(when (> livie-current-page 1)
|
|
(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))))
|
|
|
|
(defun livie-watch-this-video ()
|
|
"Stream video at point in mpv."
|
|
(interactive)
|
|
(if (equal (livie--get-entry-type (livie-get-current-video)) 'video)
|
|
(let* ((video (livie-get-current-video))
|
|
(id (livie-video-id video))
|
|
(quality-val (completing-read "Max height resolution for default is 480 (0 for unlimited): "
|
|
'("default" "0" "240" "360" "480" "720" "1080")
|
|
nil nil)))
|
|
|
|
(if (not (equal quality-val "default"))
|
|
(setq quality-val (string-to-number quality-val))
|
|
(setq quality-val 480))
|
|
|
|
(if (equal quality-val 0)
|
|
(setq quality-arg "")
|
|
(setq quality-arg (format "--ytdl-format=[height<=?%s]" quality-val)))
|
|
|
|
(start-process "livie-mpv" nil "mpv" (format "https://www.youtube.com/watch?v=%s" id) quality-arg)
|
|
(message "Opening [youtube] %s with height≤%s with mpv..." id quality-val))
|
|
(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."
|
|
(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))
|
|
|
|
;; 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)))))
|
|
(unless (= exit-code 0)
|
|
(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 ((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)
|
|
|
|
;;; livie.el ends here
|