aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesús <heckyel@hyperbola.info>2021-01-16 22:18:30 -0500
committerJesús <heckyel@hyperbola.info>2021-01-16 22:18:30 -0500
commite5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad (patch)
treec51aa26d4704d83bce13bdfe6ec5672c83a53c0f
parentbe65320ce695a59d427e56f261c4af2c3170a65f (diff)
downloadlivie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.tar.lz
livie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.tar.xz
livie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.zip
Remove python dependencie and integrate Invidious API
-rw-r--r--README.md23
-rw-r--r--livie.el358
-rw-r--r--livie.py41
-rw-r--r--screenshot.pngbin99854 -> 163105 bytes
4 files changed, 257 insertions, 165 deletions
diff --git a/README.md b/README.md
index 62928e0..2e87d10 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,11 @@ Livie allows the user to search youtube.com and play the video from `mpv`.
## Requirements
-- `python >= 3.5`
-- `python-requests`
- `hypervideo`
+- `curl`
- `mpv`
- `sudo pacman -S python mpv python-requests hypervideo`
+ `sudo pacman -S mpv hypervideo`
## Installation
@@ -30,7 +29,7 @@ Create new dir:
Clone repo:
- git clone https://libregit.org/heckyel/livie.git ~/.emacs.d/private/livie
+ git clone https://git.sr.ht/~heckyel/livie ~/.emacs.d/private/livie
Open `settings.el` write the following:
@@ -40,7 +39,17 @@ Open `settings.el` write the following:
```
## Usage
-
Just run `M-x livie` and enter a search query. `n`, `p` and `tab`
-can be used to navigate the buffer. Type `s` to enter another search.
-To watch a video, press `<enter>`.
+can be used to navigate the buffer.
+
+| key | binding |
+|-------------------|------------------------------|
+| <key>n</key> | `next-line` |
+| <key>p</key> | `previous-line` |
+| <key>q</key> | `livie-quit` |
+| <key>s</key> | `livie-search` |
+| <key>></key> | `livie-search-next-page` |
+| <key><</key> | `livie-search-previous-page` |
+| <key>return</key> | `livie-watch-this-video` |
+
+Type `s` to enter another search. To watch a video, press `<enter>`.
diff --git a/livie.el b/livie.el
index 226d3f7..5039247 100644
--- a/livie.el
+++ b/livie.el
@@ -1,155 +1,279 @@
;;; livie.el --- Livie is Video in Emacs -*- lexical-binding: t; -*-
-;; Copyright (C) 2018
+;; Copyright (C) 2018 - 2021
;;; Authors:
;; Charlie Ritter <chewzerita@posteo.net>
;; Jesus E. <heckyel@hyperbola.info>
+;; Gabriele Rastello <gabriele.rastello@edu.unito.it>
;;; Commentary:
-;; livie grabs a list of youtube videos based on a search. the user
-;; can then select a video to watch through `livie-player'
-
+;; 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 'f)
+(require 'cl-lib)
+(require 'json)
+(require 'seq)
(defgroup livie '()
"Livie is Video in Emacs"
:prefix "livie-"
:group 'livie)
-(defcustom livie-player "mpv"
- "Default video player to use with livie."
- :group 'livie
- :type 'string)
+(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-player-args '()
- "Command line arguments for `livie-player'."
- :group 'livie
- :type '(list)
- :tag "Livie Player Arguments")
+(defvar livie-invidious-api-url "https://invidious.048596.xyz"
+ "URL to Invidious instance.")
-(defcustom livie-python-name "python3"
- "Name of the python executable."
- :group 'livie
- :type 'string)
+(defvar livie-invidious-default-query-fields "author,lengthSeconds,title,videoId,authorId,viewCount,published"
+ "Default fields of interest for video search.")
-(defcustom livie-script-path nil
- "Full path of livie.py."
- :group 'livie
- :type 'file)
+(defvar livie-videos '()
+ "List of videos currently on display.")
-(defcustom livie-buffer-name "*livie*"
- "Name of the buffer to show results."
- :group 'livie
- :type 'string)
+(defvar 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.")
-(defvar livie-youtube-regexp "https://www.youtube.com/watch\\?v=[A-Za-z0-9_\\-]\\{11\\}")
+(defvar-local livie-current-page 1
+ "Current page of the current `livie-search-term'")
-(define-derived-mode livie-mode
- special-mode "livie"
- "Major mode for livie.")
+(defvar-local livie-search-term ""
+ "Current search string as used by `livie-search'")
-(defun livie-close-window ()
- "Close the livie window and bury the buffer."
- (interactive)
- (bury-buffer)
- (delete-window)
- (message nil))
+(defvar 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).")
-(defun livie-next-video ()
- "Goto the next video in the buffer."
- (interactive)
- (forward-line 1)
- (search-forward-regexp livie-youtube-regexp)
- (livie-prev-video))
+(defvar 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).")
+
+(defface livie-video-published-face
+ '((((class color) (background light)) (:foreground "#00C853"))
+ (((class color) (background dark)) (:foreground "#00E676")))
+ "Face used for the video published date.")
+
+(defface livie-channel-name-face
+ '((((class color) (background light)) (:foreground "#FFC400"))
+ (((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.")
-(defun livie-prev-video ()
- "Goto the previous video in the buffer."
+(defface livie-video-view-face
+ '((((class color) (background light)) (:foreground "#00695C"))
+ (((class color) (background dark)) (:foreground "#00BFA5")))
+ "Face used for the video views.")
+
+(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 (kbd "<return>") 'livie-watch-this-video)
+ map)
+ "Keymap for `livie-mode'.")
+
+(define-derived-mode livie-mode text-mode
+ "livie-mode"
+ (setq buffer-read-only t)
+ (buffer-disable-undo)
+ (make-local-variable 'livie-videos))
+
+(defun livie-quit ()
+ "Quit livie buffer."
(interactive)
- (search-backward-regexp livie-youtube-regexp))
+ (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 (seq-subseq name 0 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 (seq-subseq title 0 livie-title-video-reserved-space)
+ "..."))))
+ formatted-string))
+
+(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)
+ ":"
+ (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 (concat "[views:" (number-to-string views) "]") 'face 'livie-video-view-face))
-(defun livie-this-video ()
- "Go to the start of the current video."
+(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--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))))
+
+(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)
+ ", page "
+ (number-to-string livie-current-page)))
+ (seq-do (lambda (v)
+ (livie--insert-video v)
+ (insert "\n"))
+ livie-videos)
+ (goto-char (point-min))))
+
+(defun livie-search (query)
+ "Search youtube for `QUERY', and redraw the buffer."
+ (interactive "sSearch: ")
+ (setf livie-current-page 1)
+ (setf livie-search-term query)
+ (setf livie-videos (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)
- (move-beginning-of-line nil)
- (ignore-errors (forward-line -1))
- (livie-next-video))
+ (setf livie-videos (livie--query livie-search-term
+ (1+ livie-current-page)))
+ (setf livie-current-page (1+ livie-current-page))
+ (livie--draw-buffer))
-(defun livie-copy-video ()
- "Copy the currently selected video into the kill ring."
+(defun livie-search-previous-page ()
+ "Switch to the previous page of the current search. Redraw the buffer."
(interactive)
- (livie-this-video)
- (push-mark)
- (move-end-of-line nil)
- (kill-ring-save nil nil t)
- (livie-this-video))
+ (when (> livie-current-page 1)
+ (setf livie-videos (livie--query livie-search-term
+ (1- livie-current-page)))
+ (setf livie-current-page (1- 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 ()
- "Watch video under the cursor."
+ "Stream video at point in mpv."
(interactive)
- (livie-copy-video)
- (livie-watch (car kill-ring))
- (delete-other-windows))
-
-(defun livie-watch (url)
- "Watch video at URL using `livie-player' and `livie-player-args'."
- (interactive "sURL: ")
- (apply 'start-process
- (cl-concatenate
- 'list
- (list "livie"
- nil
- livie-player)
- livie-player-args
- (list url)))
- (message "Loading video..."))
-
-(defun livie (query)
- "Livie is Video in Emacs.
-
-Livie will prompt the user for a QUERY. This is fed into a
-python script that scrapes youtube.com for search results. The
-results are displayed in an Emacs buffer.
-
-See also: `livie-mode'."
- (interactive "sSearch: ")
- (unless livie-script-path
- (error "Please set `livie-script-path'"))
- (if (equal (buffer-name) livie-buffer-name)
- (progn
- (read-only-mode -1)
- (erase-buffer))
- (progn
- (select-window (split-window))
- (ignore-errors (kill-buffer livie-buffer-name))
- (switch-to-buffer (generate-new-buffer livie-buffer-name))))
- (call-process livie-python-name
- nil t t
- livie-script-path
- query)
- (livie-mode)
- (font-lock-ensure)
- (goto-char (point-min))
- (message (concat "Results for: " query)))
-
-(font-lock-add-keywords
- 'livie-mode
- `((,livie-youtube-regexp . 'link)
- ("title: \\(.*\\)" 1 'bold)
- ("channel: \\(.*\\)" 1 'italic)
- ("^ +[a-zA-Z]+:" . 'shadow)))
-
-(define-key livie-mode-map "s" 'livie)
-(define-key livie-mode-map "q" 'livie-close-window)
-(define-key livie-mode-map (kbd "<tab>") 'livie-next-video)
-(define-key livie-mode-map (kbd "<backtab>") 'livie-prev-video)
-(define-key livie-mode-map "n" 'livie-next-video)
-(define-key livie-mode-map "p" 'livie-prev-video)
-(define-key livie-mode-map (kbd "<return>") 'livie-watch-this-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")
+ (delete-other-windows)
+ (message "Starting streaming..."))
+
+(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))
+
+(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"))
+ (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))
(provide 'livie)
diff --git a/livie.py b/livie.py
deleted file mode 100644
index 50e3646..0000000
--- a/livie.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""This module does render video"""
-
-import sys
-import json
-import requests
-
-URL = 'https://youtube-scrape.herokuapp.com'
-INPUT = sys.argv[1]
-SEARCH = '%s/api/search?q=%s' % (URL, INPUT)
-REQUEST = requests.get(SEARCH)
-FIRST = True
-
-data = json.loads(REQUEST.content.decode('utf-8'))
-items = data['results']
-
-# with open('output.json', 'w') as json_file:
-# json.dump(items, json_file)
-
-for item in items:
- try:
- title = item['video']['title']
- link = item['video']['url']
- author = item['uploader']['username']
- time = item['video']['duration']
- uploaded = item['video']['upload_date']
- views = item['video']['views']
- except KeyError:
- continue
-
- if FIRST:
- FIRST = False
- else:
- print() # print skip line
-
- # prints
- print(' title: %s' % title)
- print(' url: %s' % link)
- print(' channel: %s' % author)
- print(' uploaded: %s' % uploaded)
- print(' time: %s' % time)
- print(' views: %s' % views)
diff --git a/screenshot.png b/screenshot.png
index d616062..248a0fe 100644
--- a/screenshot.png
+++ b/screenshot.png
Binary files differ