Remove python dependencie and integrate Invidious API
This commit is contained in:
parent
be65320ce6
commit
e5ab4fa1eb
23
README.md
23
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>`.
|
||||
|
350
livie.el
350
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."
|
||||
(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).")
|
||||
|
||||
(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.")
|
||||
|
||||
(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)
|
||||
(bury-buffer)
|
||||
(delete-window)
|
||||
(message nil))
|
||||
(quit-window))
|
||||
|
||||
(defun livie-next-video ()
|
||||
"Goto the next video in the buffer."
|
||||
(interactive)
|
||||
(forward-line 1)
|
||||
(search-forward-regexp livie-youtube-regexp)
|
||||
(livie-prev-video))
|
||||
(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-prev-video ()
|
||||
"Goto the previous video in the buffer."
|
||||
(interactive)
|
||||
(search-backward-regexp livie-youtube-regexp))
|
||||
(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-this-video ()
|
||||
"Go to the start of the current video."
|
||||
(interactive)
|
||||
(move-beginning-of-line nil)
|
||||
(ignore-errors (forward-line -1))
|
||||
(livie-next-video))
|
||||
(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-copy-video ()
|
||||
"Copy the currently selected video into the kill ring."
|
||||
(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--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)
|
||||
(livie-this-video)
|
||||
(push-mark)
|
||||
(move-end-of-line nil)
|
||||
(kill-ring-save nil nil t)
|
||||
(livie-this-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-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--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))
|
||||
(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-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-buffer ()
|
||||
"Name for the main livie buffer."
|
||||
(get-buffer-create "*livie*"))
|
||||
|
||||
(defun livie (query)
|
||||
"Livie is Video in Emacs.
|
||||
;;;###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)))
|
||||
|
||||
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.
|
||||
;; 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))
|
||||
|
||||
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)))
|
||||
(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))))
|
||||
|
||||
(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)
|
||||
(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)
|
||||
|
||||
|
41
livie.py
41
livie.py
@ -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)
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 159 KiB |
Loading…
x
Reference in New Issue
Block a user