aboutsummaryrefslogtreecommitdiff
path: root/emacs/.emacs.d/lisp/my/my-ytdl.el
blob: b3b1cf7e09fcc7c660fd20ec4032a58698b57b81 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
;;; my-ytdl.el -- ytdl client -*- lexical-binding: t -*-

;; Copyright (C) 2023 Free Software Foundation.

;; Author: Yuchen Pei <id@ypei.org>
;; Package-Requires: ((emacs "28.2"))

;; This file is part of dotfiles.

;; dotfiles is free software: you can redistribute it and/or modify it under
;; the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; dotfiles is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
;; Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with dotfiles.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; ytdl client. Works with youtube-dl, yt-dlp etc.

;;; Code:


(defvar my-ytdl-program "yt-dlp")

(defvar my-ytdl-video-args
  '("--download-archive" "yt-dlp-archive"
    ;; Get rid of silly full-width chars and emojis
    ;; https://old.reddit.com/r/youtubedl/comments/yz9ozo/how_do_i_get_ytdlp_downloads_without_forbidden/
    "--replace-in-metadata" "title"
    "[\U0000002A\U0000005C\U0000002F\U0000003A\U00000022\U0000003F\U0000007C\U00010000-\U0010FFFF]" "_"
    ;; truncate filename length, but it will not work with the
    ;; following file name format, as it will replace the leading ./
    ;; with /
    ;; "--trim-filenames" "200"
    "-o"
    "%(playlist|.)s/%(playlist_index|)s%(playlist_index&-|)s%(title)s.%(ext)s"    ;; https://github.com/yt-dlp/yt-dlp/issues/5630
    ;;    "%(id)s.%(ext)s" ;; alternative for long names
    "-f" "bv*[height<=?720]+ba/best[height<=?720]"
    "--write-subs" "--sub-langs" "en"
    "--write-description"
    "--write-thumbnail"))

(defvar my-ytdl-video-download-dir "~/Downloads"
  "Directory for ytdl to download videos to.")

(defvar my-ytdl-audio-args
  '("-x" "--download-archive" "yt-dlp-archive"
    ;; Get rid of silly full-width chars and emojis
    ;; https://old.reddit.com/r/youtubedl/comments/yz9ozo/how_do_i_get_ytdlp_downloads_without_forbidden/
    "--replace-in-metadata" "title"
    "[\U0000002A\U0000005C\U0000002F\U0000003A\U00000022\U0000003F\U0000007C\U00010000-\U0010FFFF]" "_"
    "-o"
    ;; "%(id)s.%(ext)s" ;; for long names
    "%(playlist|.)s/%(playlist_index|)s%(playlist_index&-|)s%(title)s.%(ext)s"
    "--write-description"
    "--write-thumbnail"))

(defvar my-ytdl-audio-download-dir "~/Downloads"
  "Directory for ytdl to download audios to.")

(defun my-ytdl-internal (urls type &optional no-tor)
  (my-with-default-directory (if (eq type 'video)
                                 my-ytdl-video-download-dir
                               my-ytdl-audio-download-dir)
    (apply 'my-start-process-with-torsocks
           (append
            (list no-tor (format "ytdl-%s" urls) (format "*ytdl-%s*" urls)
                  my-ytdl-program)
            (if (eq type 'video) my-ytdl-video-args my-ytdl-audio-args)
            (split-string urls)))))

(defun my-ytdl-video-info (url)
  "Given a video URL, return an alist of its properties."
  (with-temp-buffer
    (call-process my-ytdl-program nil t nil "--no-warnings" "-j" url)
    (let ((start (point)))
      (call-process-region
       nil nil "jq" nil t nil
       "pick(.webpage_url, .fulltitle, .channel_url, .channel, .channel_follower_count, .thumbnail, .duration_string, .view_count, .upload_date, .like_count, .is_live, .was_live, .categories, .tags, .chapters, .availability, .uploader, .description)")
      (goto-char start)
      (json-read)))
  )

(defun my-ytdl-video-url-p (url)
  (let ((urlobj (url-generic-parse-url url)))
    (or (and (string-match-p
              "^\\(www\\.\\)?\\(youtube\\.com\\|yewtu\\.be\\)"
              (url-host urlobj))
             (string-match-p "^/watch\\?v=.*" (url-filename urlobj)))
        (equal "youtu.be" (url-host urlobj)))))

(require 'hmm)
(defvar my-ytdl-player 'hmm-external-mpv "Function to play ytdl urls.")

(defun my-ytdl-video-format-seconds (secs)
  (setq secs (floor secs))
  (if (>= secs 3600)
      (format "%d:%02d:%02d"
              (/ secs 3600) (/ (% secs 3600) 60) (% secs 60))
    (format "%d:%02d"
            (/ secs 60) (% secs 60))))

(defun my-ytdl-video-format-chapters (chapters)
  (mapconcat
   (lambda (chapter)
     (let-alist chapter
       (format "%s: %s-%s" .title (my-ytdl-video-format-seconds .start_time)
               (my-ytdl-video-format-seconds .end_time))))
   chapters
   "; "))

(defun my-ytdl-video-render-info (info url)
  (setf (alist-get 'webpage_url info)
        (concat (alist-get 'webpage_url info)
                " -- " (buttonize "play" (lambda (_)
                                           (funcall my-ytdl-player url)))
                " " (buttonize "context"
                               (lambda (_)
                                 (funcall my-url-context-function url))))
        (alist-get 'chapters info)
        (my-ytdl-video-format-chapters (alist-get 'chapters info)))
  (infobox-render
   (infobox-translate info (infobox-default-specs info))
   `(my-ytdl-video-infobox ,url)
   (called-interactively-p 'interactive)))

(defun my-ytdl-video-infobox (url)
  (interactive "sytdl video url: ")
  ;; Remove any extra queries from the URL
  (setq url (replace-regexp-in-string "&.*" "" url))
  (my-ytdl-video-render-info (my-ytdl-video-info url) url))

;;; fixme: autoload
(defun my-ytdl-video (urls)
  "Download videos with ytdl."
  (interactive "sURL(s): ")
  (my-ytdl-internal urls 'video))

(defun my-ytdl-audio (urls)
  "Download audio with ytdl."
  (interactive "sURL(s): ")
  (my-ytdl-internal urls 'audio))

(defun my-ytdl-audio-no-tor (urls)
  "Download audio with ytdl."
  (interactive "sURL(s): ")
  (my-ytdl-internal urls 'audio t))

;;; fixme: autoload
(defun my-ytdl-video-no-tor (urls)
  "Download videos with ytdl."
  (interactive "sURL(s): ")
  (my-ytdl-internal urls 'video t))

(provide 'my-ytdl)
;;; my-ytdl.el ends here