livie.el 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. ;;; livie.el --- Livie is Video in Emacs -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2018 - 2021
  3. ;; Version: 1.0.0
  4. ;;; Authors:
  5. ;; Charlie Ritter <chewzerita@posteo.net>
  6. ;; Jesus E. <heckyel@hyperbola.info>
  7. ;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
  8. ;; Pablo BC <pablo.barraza@protonmail.com>
  9. ;;; Commentary:
  10. ;; livie grabs a list of youtube videos based on a search.
  11. ;; the user can then select a video to watch through `livie-player'
  12. ;;; Code:
  13. (require 'cl-lib)
  14. (require 'json)
  15. (require 'seq)
  16. (declare-function livie-channel 'livie-channel)
  17. (declare-function livie--get-playlist-videos 'livie-playlist)
  18. (defgroup livie '()
  19. "Livie is Video in Emacs"
  20. :prefix "livie-"
  21. :group 'livie)
  22. (defcustom livie-sort-criterion 'relevance
  23. "Criteria to sort the results of the search query."
  24. :type 'symbol
  25. :options '(relevance rating upload_date view_count)
  26. :group 'livie)
  27. (defcustom livie-type-of-results "video"
  28. "Set what type of results to get when making a search."
  29. :type 'string
  30. :options '("video" "playlist" "channel" "all")
  31. :group 'livie)
  32. (defcustom livie-show-fancy-icons nil
  33. "If t, enable showing fancy icons in the search buffer."
  34. :type 'boolean
  35. :group 'livie)
  36. ;; TODO: Try to add support using all-the-icons, or add images instead.
  37. (defcustom livie-icons '((video "Video" "✇")
  38. (playlist "Playlist" "🎞")
  39. ;; Added a space to this icon so everything is aligned
  40. (channel "Channel" "📺 ")
  41. (length "" "⌚:")
  42. (views "views" "👁")
  43. (subCount "subscribers" "🅯")
  44. (videoCount "videos" "▶"))
  45. "Icons for displaying items in buffer. First string is inserted if `livie-show-fancy-icons' is disabled."
  46. :type '(alist :value-type (group string string))
  47. :group 'livie)
  48. (defvar livie-invidious-api-url "https://invidious.namazso.eu"
  49. "URL to Invidious instance.")
  50. (defvar livie--insert-functions '((video . livie--insert-video)
  51. (playlist . livie--insert-playlist)
  52. (channel . livie--insert-channel)))
  53. (defvar livie--default-action-functions '((video . livie--default-video-action)
  54. (playlist . livie--default-playlist-action)
  55. (channel . livie--default-channel-action))
  56. "Functions to call on an entry. To modify an action, set the appropiate variable instead.")
  57. (defvar livie--default-video-action #'(lambda ()
  58. (message (livie-video-title (livie-get-current-video))))
  59. "Action to open a video. By default it just prints the title to the minibuffer.")
  60. (defvar livie--default-playlist-action #'livie--open-playlist
  61. "Action to open a playlist.")
  62. (defvar livie--default-channel-action #'livie--open-channel
  63. "Action to open a channel.")
  64. (defvar livie-default-video-query-fields "type,author,lengthSeconds,title,videoId,authorId,viewCount,published"
  65. "Default fields of interest for video search.")
  66. (defvar livie-default-channel-query-fields "type,author,authorId,subCount,videoCount"
  67. "Default fields of interest for channel search.")
  68. (defvar livie-default-playlist-query-fields "type,title,playlistId,author,authorId,videoCount"
  69. "Default fields of interest for playlist search.")
  70. (defvar livie-videos '()
  71. "List of videos currently on display.")
  72. (defcustom livie-published-date-time-string "%Y-%m-%d"
  73. "Time-string used to render the published date of the video.
  74. See `format-time-string' for information on how to edit this variable."
  75. :type 'string
  76. :group 'livie)
  77. (defvar-local livie-current-page 1
  78. "Current page of the current `livie-search-term'")
  79. (defvar-local livie-search-term ""
  80. "Current search string as used by `livie-search'")
  81. (defcustom livie-author-name-reserved-space 20
  82. "Number of characters reserved for the channel's name in the *livie* buffer.
  83. Note that there will always 3 extra spaces for eventual dots (for names that are
  84. too long)."
  85. :type 'integer
  86. :group 'livie)
  87. (defcustom livie-title-video-reserved-space 100
  88. "Number of characters reserved for the video title in the *livie* buffer.
  89. Note that there will always 3 extra spaces for eventual dots (for names that are
  90. too long)."
  91. :type 'integer
  92. :group 'livie)
  93. (defcustom livie-title-playlist-reserved-space 30
  94. "Number of characters reserved for the playlist title in the *livie* buffer.
  95. Note that there will always 3 extra spaces for eventual dots (for names that are
  96. too long)."
  97. :type 'integer
  98. :group 'livie)
  99. (defcustom livie-name-channel-reserved-space 50
  100. "Number of characters reserved for the channel name in the *livie* buffer.
  101. Note that there will always 3 extra spaces for eventual dots (for names that are
  102. too long)."
  103. :type 'integer
  104. :group 'livie)
  105. (defface livie-video-published-face
  106. '((((class color) (background light)) (:foreground "#1B5E20"))
  107. (((class color) (background dark)) (:foreground "#00E676")))
  108. "Face used for the video published date.")
  109. (defface livie-channel-name-face
  110. '((((class color) (background light)) (:foreground "#FF6D00"))
  111. (((class color) (background dark)) (:foreground "#FFFF00")))
  112. "Face used for channel names.")
  113. (defface livie-video-length-face
  114. '((((class color) (background light)) (:foreground "#6A1B9A"))
  115. (((class color) (background dark)) (:foreground "#AA00FF")))
  116. "Face used for the video length.")
  117. (defface livie-video-view-face
  118. '((((class color) (background light)) (:foreground "#00695C"))
  119. (((class color) (background dark)) (:foreground "#00BFA5")))
  120. "Face used for the video views.")
  121. (defface livie-video-title-face
  122. '((((class color) (background light)) (:foreground "#000000"))
  123. (((class color) (background dark)) (:foreground "#FFFFFF")))
  124. "Face used for the video title.")
  125. (defface livie-item-videoCount-face
  126. '((t :inherit livie-video-view-face))
  127. "Face used for the videoCount of an entry.")
  128. (defface livie-item-subCount-face
  129. '((t :inherit livie-video-published-face))
  130. "Face used for the subCount of an entry.")
  131. (defface livie-parameter-face
  132. '((t :inherit livie-video-published-face))
  133. "Face used for the parameters of the current search.")
  134. (defvar livie-mode-map
  135. (let ((map (make-sparse-keymap)))
  136. (suppress-keymap map)
  137. (define-key map "q" #'livie-quit)
  138. (define-key map "h" #'describe-mode)
  139. (define-key map "n" #'next-line)
  140. (define-key map "p" #'previous-line)
  141. (define-key map (kbd "<tab>") #'next-line)
  142. (define-key map (kbd "<backtab>") #'previous-line)
  143. (define-key map "s" #'livie-search)
  144. (define-key map ">" #'livie-search-next-page)
  145. (define-key map "<" #'livie-search-previous-page)
  146. (define-key map "t" #'livie-search-type)
  147. (define-key map "S" #'livie-sort-videos)
  148. (define-key map "C" #'livie-show-channels)
  149. (define-key map "P" #'livie-show-playlists)
  150. (define-key map "V" #'livie-show-videos)
  151. (define-key map "Y" #'livie-yank-channel-feed)
  152. (define-key map "A" #'livie--open-channel)
  153. (define-key map (kbd "RET") #'livie-open-entry)
  154. (define-key map "y" #'livie-watch-this-video)
  155. map)
  156. "Keymap for `livie-mode'.")
  157. (define-derived-mode livie-mode text-mode
  158. "livie-mode"
  159. "A major mode to query Youtube content through Invidious."
  160. :group 'livie
  161. (setq buffer-read-only t)
  162. (buffer-disable-undo)
  163. (make-local-variable 'livie-videos))
  164. (defun livie-quit ()
  165. "Quit livie buffer."
  166. (interactive)
  167. (kill-buffer))
  168. (defun livie--format-author (name)
  169. "Format a channel NAME to be inserted in the *livie* buffer."
  170. (let* ((n (string-width name))
  171. (extra-chars (- n livie-author-name-reserved-space))
  172. (formatted-string (if (<= extra-chars 0)
  173. (concat name
  174. (make-string (abs extra-chars) ?\ )
  175. " ")
  176. (concat (truncate-string-to-width name livie-author-name-reserved-space)
  177. "..."))))
  178. (propertize formatted-string 'face 'livie-channel-name-face)))
  179. (defun livie--format-title (title)
  180. "Format a video TITLE to be inserted in the *livie* buffer."
  181. (let* ((n (string-width title))
  182. (extra-chars (- n livie-title-video-reserved-space))
  183. (formatted-string (if (<= extra-chars 0)
  184. (concat title
  185. (make-string (abs extra-chars) ?\ )
  186. " ")
  187. (concat (truncate-string-to-width title livie-title-video-reserved-space)
  188. "..."))))
  189. (propertize formatted-string 'face 'livie-video-title-face)))
  190. (defun livie--format-playlist-title (title)
  191. "Format a playlist TITLE to be inserted in the *livie* buffer."
  192. (let* ((n (string-width title))
  193. (extra-chars (- n livie-title-playlist-reserved-space))
  194. (formatted-string (if (<= extra-chars 0)
  195. (concat title
  196. (make-string (abs extra-chars) ?\ )
  197. " ")
  198. (concat (truncate-string-to-width title livie-title-playlist-reserved-space)
  199. "..."))))
  200. (propertize formatted-string 'face 'livie-video-title-face)))
  201. (defun livie--format-channel-name (name)
  202. "Format a channel NAME to be inserted in the *livie* buffer."
  203. (let* ((n (string-width name))
  204. (extra-chars (- n livie-name-channel-reserved-space))
  205. (formatted-string (if (<= extra-chars 0)
  206. (concat name
  207. (make-string (abs extra-chars) ?\ )
  208. " ")
  209. (concat (truncate-string-to-width name livie-name-channel-reserved-space)
  210. "..."))))
  211. (propertize formatted-string 'face 'livie-channel-name-face)))
  212. (defun livie--format-video-length (seconds)
  213. "Given an amount of SECONDS, format it nicely to be inserted in the *livie* buffer."
  214. (let ((formatted-string (concat (livie--get-icon 'length)
  215. (format-seconds "%.2h" seconds)
  216. ":"
  217. (format-seconds "%.2m" (mod seconds 3600))
  218. ":"
  219. (format-seconds "%.2s" (mod seconds 60)))))
  220. (propertize formatted-string 'face 'livie-video-length-face)))
  221. (defun livie--format-video-views (views)
  222. "Format video VIEWS to be inserted in the *livie* buffer."
  223. (propertize (format "[%s: %d]" (livie--get-icon 'views) views) 'face 'livie-video-view-face))
  224. (defun livie--format-video-published (published)
  225. "Format video PUBLISHED date to be inserted in the *livie* buffer."
  226. (propertize (format-time-string livie-published-date-time-string (seconds-to-time published))
  227. 'face 'livie-video-published-face))
  228. (defun livie--format-videoCount (videoCount)
  229. "Format video VIDEOCOUNT to be inserted in the *livie* buffer."
  230. (propertize (format "[%s: %d]" (livie--get-icon 'videoCount) videoCount) 'face 'livie-item-videoCount-face))
  231. (defun livie--format-subCount (subCount)
  232. "Format video SUBCOUNT to be inserted in the *livie* buffer."
  233. (propertize (format "%s: %-10d" (livie--get-icon 'subCount) subCount) 'face 'livie-item-subCount-face))
  234. (defun livie--format-type (type)
  235. "Insert an icon of TYPE into buffer."
  236. (if livie-show-fancy-icons
  237. (propertize (format "%-2s: " (livie--get-icon type)) 'face 'livie-video-title-face)
  238. (propertize (format "%-10s: " (livie--get-icon type)) 'face 'livie-video-title-face)))
  239. (defun livie--get-icon (item)
  240. "Get the icon for ITEM from `livie-icons'."
  241. (let* ((getmarks (assoc-default item livie-icons)))
  242. (if livie-show-fancy-icons
  243. (when (fboundp 'second)
  244. (second getmarks))
  245. (car getmarks))))
  246. (defun livie--insert-entry (entry)
  247. "Insert an ENTRY of the form according to its type."
  248. (let* ((type (if (not (equal livie-type-of-results "all"))
  249. (intern livie-type-of-results)
  250. (cond ((livie-video-p entry) 'video)
  251. ((livie-playlist-p entry) 'playlist)
  252. ((livie-channel-p entry) 'channel)
  253. (t (error "Invalid entry type")))))
  254. (func (cdr (assoc type livie--insert-functions))))
  255. (when (equal livie-type-of-results "all")
  256. (insert (livie--format-type type)))
  257. (funcall func entry)))
  258. (defun livie--insert-video (video)
  259. "Insert VIDEO in the current buffer."
  260. (insert (livie--format-video-published (livie-video-published video))
  261. " "
  262. (livie--format-author (livie-video-author video))
  263. " "
  264. (livie--format-video-length (livie-video-length video))
  265. " "
  266. (livie--format-title (livie-video-title video))
  267. " "
  268. (livie--format-video-views (livie-video-views video))))
  269. ;; TODO: Format playlist and channel entries in buffer
  270. (defun livie--insert-playlist (playlist)
  271. "Insert PLAYLIST in the current buffer."
  272. (insert (livie--format-playlist-title (livie-playlist-title playlist))
  273. " "
  274. (livie--format-author (livie-playlist-author playlist))
  275. " "
  276. (livie--format-videoCount (livie-playlist-videoCount playlist))))
  277. (defun livie--insert-channel (channel)
  278. "Insert CHANNEL in the current buffer."
  279. (insert (livie--format-channel-name (livie-channel-author channel))
  280. " "
  281. (livie--format-subCount (livie-channel-subCount channel))
  282. " "
  283. (livie--format-videoCount (livie-channel-videoCount channel))))
  284. (defun livie--draw-buffer ()
  285. "Draws the livie buffer i.e. clear everything and write down all videos in `livie-videos'."
  286. (let ((inhibit-read-only t))
  287. (erase-buffer)
  288. (setf header-line-format (concat (propertize (capitalize livie-type-of-results) 'face 'livie-parameter-face)
  289. " results for "
  290. (propertize livie-search-term 'face 'livie-parameter-face)
  291. ", page "
  292. (propertize (number-to-string livie-current-page) 'face 'livie-parameter-face)
  293. ", sorted by: "
  294. (propertize (symbol-name livie-sort-criterion) 'face 'livie-parameter-face)))
  295. (seq-do (lambda (v)
  296. (livie--insert-entry v)
  297. (insert "\n"))
  298. livie-videos)
  299. (goto-char (point-min))))
  300. (defun livie-enable-fancy-icons ()
  301. "Enable fancy icons in the *livie* buffer, using `livie-icons'."
  302. (interactive)
  303. (setf livie-show-fancy-icons t))
  304. (defun livie-disable-fancy-icons ()
  305. "Disable fancy icons in the *livie* buffer, using `livie-icons'."
  306. (interactive)
  307. (setf livie-show-fancy-icons nil))
  308. (defun livie-toggle-fancy-icons ()
  309. "Toggle display of fancy-icons in the *livie* buffer, using `livie-icons'."
  310. (interactive)
  311. (setf livie-show-fancy-icons (not livie-show-fancy-icons)))
  312. (defun livie-search (query)
  313. "Search youtube for `QUERY', and redraw the buffer."
  314. (interactive "sSearch: ")
  315. (switch-to-buffer "*livie*")
  316. (setf livie-current-page 1)
  317. (setf livie-search-term query)
  318. (setf livie-videos (livie--process-results (livie--query query livie-current-page)))
  319. (livie--draw-buffer))
  320. (defun livie-search-next-page ()
  321. "Switch to the next page of the current search. Redraw the buffer."
  322. (interactive)
  323. (setf livie-videos (livie--process-results (livie--query livie-search-term
  324. (1+ livie-current-page))))
  325. (setf livie-current-page (1+ livie-current-page))
  326. (livie--draw-buffer))
  327. (defun livie-search-previous-page ()
  328. "Switch to the previous page of the current search. Redraw the buffer."
  329. (interactive)
  330. (when (> livie-current-page 1)
  331. (setf livie-videos (livie--process-results (livie--query livie-search-term
  332. (1- livie-current-page))))
  333. (setf livie-current-page (1- livie-current-page))
  334. (livie--draw-buffer)))
  335. (defun livie-search-type (&optional arg)
  336. "Ask for what type of results to display, and search.
  337. If ARG is given, make a new search."
  338. (interactive "P")
  339. (when arg
  340. (setf livie-search-term (read-string "Search: ")))
  341. (setf livie-current-page 1)
  342. (setf livie-type-of-results (completing-read "Show: " (get 'livie-type-of-results 'custom-options)))
  343. (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  344. (livie--draw-buffer))
  345. (defun livie-show-videos (&optional arg)
  346. "Show videos for the current search.
  347. If ARG is given, make a new search."
  348. (interactive "P")
  349. (when arg
  350. (setf livie-search-term (read-string "Search: ")))
  351. (setf livie-current-page 1)
  352. (setf livie-type-of-results "video")
  353. (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  354. (livie--draw-buffer))
  355. (defun livie-show-channels (&optional arg)
  356. "Show channels for the current search.
  357. If ARG is given, make a new search."
  358. (interactive "P")
  359. (when arg
  360. (setf livie-search-term (read-string "Search: ")))
  361. (setf livie-current-page 1)
  362. (setf livie-type-of-results "channel")
  363. (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  364. (livie--draw-buffer))
  365. (defun livie-show-playlists (&optional arg)
  366. "Show playlists for the current search.
  367. If ARG is given, make a new search."
  368. (interactive "P")
  369. (when arg
  370. (setf livie-search-term (read-string "Search: ")))
  371. (setf livie-current-page 1)
  372. (setf livie-type-of-results "playlist")
  373. (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  374. (livie--draw-buffer))
  375. (defun livie-sort-videos ()
  376. "Sort videos from the current search from page 1, according to values of `livie-sort-criterion'."
  377. (interactive)
  378. (setf livie-sort-criterion (intern (completing-read "Sort videos by (default value is relevance): " (get 'livie-sort-criterion 'custom-options))))
  379. (setf livie-current-page 1)
  380. (setf livie-videos (livie--process-results (livie--query livie-search-term livie-current-page)))
  381. (livie--draw-buffer))
  382. (defun livie-get-current-video ()
  383. "Get the currently selected video."
  384. (aref livie-videos (1- (line-number-at-pos))))
  385. (defun livie-watch-this-video ()
  386. "Stream video at point in mpv."
  387. (interactive)
  388. (if (equal (livie--get-entry-type (livie-get-current-video)) 'video)
  389. (let* ((video (livie-get-current-video))
  390. (id (livie-video-id video))
  391. (quality-val (completing-read "Max height resolution for default is 480 (0 for unlimited): "
  392. '("default" "0" "240" "360" "480" "720" "1080")
  393. nil nil)))
  394. (if (not (equal quality-val "default"))
  395. (setq quality-val (string-to-number quality-val))
  396. (setq quality-val 480))
  397. (if (equal quality-val 0)
  398. (setq quality-arg "")
  399. (setq quality-arg (format "--ytdl-format=[height<=?%s]" quality-val)))
  400. (start-process "livie-mpv" nil "mpv" (format "https://www.youtube.com/watch?v=%s" id) quality-arg)
  401. (message "Playing [%s/watch?v=%s] with height≤%s with mpv..." livie-invidious-api-url id quality-val))
  402. (message "It's not a video")))
  403. (defun livie-yank-channel-feed (&optional arg)
  404. "Yank channel's Youtube RSS feed for the current video at point.
  405. If ARG is given, format it as a Invidious RSS feed."
  406. (interactive "P")
  407. (let* ((entry (livie-get-current-video))
  408. (author (funcall (livie--get-author-function entry) entry))
  409. (authorId (funcall (livie--get-authorId-function entry) entry))
  410. (url (if arg
  411. (concat livie-invidious-api-url "/feed/channel/" authorId)
  412. (concat "https://www.youtube.com/feeds/videos.xml?channel_id=" authorId))))
  413. (kill-new url)
  414. (message "Copied RSS feed for: %s - %s" author url)))
  415. (defun livie--get-entry-type (entry)
  416. "Return the type of ENTRY."
  417. (if (not (equal livie-type-of-results "all"))
  418. (intern livie-type-of-results)
  419. (cond ((livie-video-p entry) 'video)
  420. ((livie-playlist-p entry) 'playlist)
  421. ((livie-channel-p entry) 'channel)
  422. (t (error "Invalid entry type")))))
  423. (defun livie--get-author-function (entry)
  424. "Get the author for ENTRY."
  425. (let* ((type (livie--get-entry-type entry)))
  426. (pcase type
  427. ('video #'livie-video-author)
  428. ('playlist #'livie-playlist-author)
  429. ('channel #'livie-channel-author)
  430. (_ (error "Invalid entry type")))))
  431. (defun livie--get-authorId-function (entry)
  432. "Get the author for ENTRY."
  433. (let* ((type (livie--get-entry-type entry)))
  434. (pcase type
  435. ('video #'livie-video-authorId)
  436. ('playlist #'livie-playlist-authorId)
  437. ('channel #'livie-channel-authorId)
  438. (_ (error "Invalid entry type")))))
  439. (defun livie-buffer ()
  440. "Name for the main livie buffer."
  441. (get-buffer-create "*livie*"))
  442. ;;;###autoload
  443. (defun livie ()
  444. "Enter livie."
  445. (interactive)
  446. (switch-to-buffer (livie-buffer))
  447. (unless (eq major-mode 'livie-mode)
  448. (livie-mode))
  449. (when (seq-empty-p livie-search-term)
  450. (call-interactively #'livie-search)))
  451. ;; Youtube interface stuff below.
  452. (cl-defstruct (livie-video (:constructor livie-video--create)
  453. (:copier nil))
  454. "Information about a Youtube video."
  455. (title "" :read-only t)
  456. (id 0 :read-only t)
  457. (author "" :read-only t)
  458. (authorId "" :read-only t)
  459. (length 0 :read-only t)
  460. (views 0 :read-only t)
  461. (published 0 :read-only t))
  462. ;; Maybe type should be part of the struct.
  463. (cl-defstruct (livie-channel (:constructor livie-channel--create)
  464. (:copier nil))
  465. "Information about a Youtube channel."
  466. (author "" :read-only t)
  467. (authorId "" :read-only t)
  468. (subCount 0 :read-only t)
  469. (videoCount 0 :read-only t))
  470. (cl-defstruct (livie-playlist (:constructor livie-playlist--create)
  471. (:copier nil))
  472. "Information about a Youtube playlist."
  473. (title "" :read-only t)
  474. (playlistId "" :read-only t)
  475. (author "" :read-only t)
  476. (authorId "" :read-only t)
  477. (videoCount 0 :read-only t))
  478. (defun livie--API-call (method args)
  479. "Perform a call to the invidious API method METHOD passing ARGS.
  480. Curl is used to perform the request. An error is thrown if it exits with a non
  481. zero exit code otherwise the request body is parsed by `json-read' and returned."
  482. (with-temp-buffer
  483. (let ((exit-code (call-process "curl" nil t nil
  484. "--silent"
  485. "-X" "GET"
  486. (concat livie-invidious-api-url
  487. "/api/v1/" method
  488. "?" (url-build-query-string args)))))
  489. (unless (= exit-code 0)
  490. (error "Curl had problems connecting to Invidious API"))
  491. (goto-char (point-min))
  492. (json-read))))
  493. (defun livie--query (string n)
  494. "Query youtube for STRING, return the Nth page of results."
  495. (let ((results (livie--API-call "search" `(("q" ,string)
  496. ("sort_by" ,(symbol-name livie-sort-criterion))
  497. ("type" ,livie-type-of-results)
  498. ("page" ,n)
  499. ("fields" ,(pcase livie-type-of-results
  500. ("video" livie-default-video-query-fields)
  501. ("playlist" livie-default-playlist-query-fields)
  502. ("channel" livie-default-channel-query-fields)
  503. ;; I mean, it does get the job done... fix later.
  504. ("all" (concat livie-default-channel-query-fields
  505. ","
  506. livie-default-playlist-query-fields
  507. ","
  508. livie-default-video-query-fields))))))))
  509. results))
  510. (defun livie--process-results (results &optional type)
  511. "Process RESULTS and turn them into objects, is TYPE is not given, get it from RESULTS."
  512. (dotimes (i (length results))
  513. (let* ((v (aref results i))
  514. (type (or type (assoc-default 'type v))))
  515. (aset results i (pcase type
  516. ("video" (livie-video--create
  517. :title (assoc-default 'title v)
  518. :author (assoc-default 'author v)
  519. :authorId (assoc-default 'authorId v)
  520. :length (assoc-default 'lengthSeconds v)
  521. :id (assoc-default 'videoId v)
  522. :views (assoc-default 'viewCount v)
  523. :published (assoc-default 'published v)))
  524. ("playlist" (livie-playlist--create
  525. :title (assoc-default 'title v)
  526. :playlistId (assoc-default 'playlistId v)
  527. :author (assoc-default 'author v)
  528. :authorId (assoc-default 'authorId v)
  529. :videoCount (assoc-default 'videoCount v)))
  530. ("channel" (livie-channel--create
  531. :author (assoc-default 'author v)
  532. :authorId (assoc-default 'authorId v)
  533. :subCount (assoc-default 'subCount v)
  534. :videoCount (assoc-default 'videoCount v)))))))
  535. results)
  536. (defun livie-open-entry ()
  537. "Open the entry at point depending on it's type."
  538. (interactive)
  539. (let* ((entry (livie-get-current-video))
  540. (type (livie--get-entry-type entry)))
  541. (funcall (symbol-value (assoc-default type livie--default-action-functions)))))
  542. (defun livie--open-channel ()
  543. "Fetch the channel page for the entry at point."
  544. (interactive)
  545. (require 'livie-channel)
  546. (livie-channel))
  547. (defun livie--open-playlist ()
  548. "Open the contents of the entry at point, if it's a playlist."
  549. (interactive)
  550. (require 'livie-playlist)
  551. (livie--get-playlist-videos))
  552. (provide 'livie)
  553. ;;; livie.el ends here