diff options
author | Jesús <heckyel@hyperbola.info> | 2021-01-16 22:18:30 -0500 |
---|---|---|
committer | Jesús <heckyel@hyperbola.info> | 2021-01-16 22:18:30 -0500 |
commit | e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad (patch) | |
tree | c51aa26d4704d83bce13bdfe6ec5672c83a53c0f | |
parent | be65320ce695a59d427e56f261c4af2c3170a65f (diff) | |
download | livie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.tar.lz livie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.tar.xz livie-e5ab4fa1eb5c0a86c6c65075f14aa3b37f5108ad.zip |
Remove python dependencie and integrate Invidious API
-rw-r--r-- | README.md | 23 | ||||
-rw-r--r-- | livie.el | 358 | ||||
-rw-r--r-- | livie.py | 41 | ||||
-rw-r--r-- | screenshot.png | bin | 99854 -> 163105 bytes |
4 files changed, 257 insertions, 165 deletions
@@ -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>`. @@ -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 Binary files differindex d616062..248a0fe 100644 --- a/screenshot.png +++ b/screenshot.png |