aboutsummaryrefslogtreecommitdiffstats
path: root/livie.el
diff options
context:
space:
mode:
Diffstat (limited to 'livie.el')
-rw-r--r--livie.el475
1 files changed, 415 insertions, 60 deletions
diff --git a/livie.el b/livie.el
index 8bdf2ff..121381f 100644
--- a/livie.el
+++ b/livie.el
@@ -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)