1 ;;; youtube-dl.el --- manages a youtube-dl queue -*- lexical-binding: t; -*-
3 ;; This is free and unencumbered software released into the public domain.
5 ;; Author: Christopher Wellons <wellons@nullprogram.com>
6 ;; URL: https://github.com/skeeto/youtube-dl-emacs
8 ;; Package-Requires: ((emacs "27.1"))
12 ;; This package manages a video download queue for the youtube-dl
13 ;; command line program, which serves as its back end. It manages a
14 ;; single youtube-dl subprocess to download one video at a time. New
15 ;; videos can be queued at any time.
17 ;; The `youtube-dl' command queues a URL for download. Failures are
18 ;; retried up to `youtube-dl-max-failures'. Items can be paused or set
19 ;; to be downloaded at a slower download rate (`youtube-dl-slow-rate-limit').
21 ;; The `youtube-dl-download-playlist' command queues an entire playlist, just
22 ;; as if you had individually queued each video on the playlist.
24 ;; The `youtube-dl-list' command displays a list of all active video
25 ;; downloads. From this list, items under point can be canceled (d),
26 ;; paused (p), slowed (s), and have its priority adjusted ([ and ]).
33 (require 'notifications
)
35 (defgroup youtube-dl
()
36 "Download queue for the youtube-dl command line program."
39 (defcustom youtube-dl-directory
"~"
40 "Directory in which to run youtube-dl."
44 (defcustom youtube-dl-program
46 ((executable-find "yt-dlp") "yt-dlp") ; "yt-dlp" is a fork of "youtube-dl".
47 ((executable-find "youtube-dl") "youtube-dl"))
48 "The name of the program invoked for downloading YouTube videos."
53 (defcustom youtube-dl-arguments
54 '("--no-mtime" "--restrict-filenames")
55 "Arguments to be send to youtube-dl.
56 Instead of --limit-rate use custom option `youtube-dl-slow-rate-limit'."
57 :type
'(repeat string
)
61 (defcustom youtube-dl-proxy
""
62 "Specify the proxy for youtube-dl command.
66 socks5://127.0.0.1:1086"
71 (defcustom youtube-dl-proxy-url-list
'()
72 "A list of URL domains which should use proxy for youtube-dl."
77 (defcustom youtube-dl-auto-show-list t
78 "Auto show youtube-dl-list buffer."
83 (defcustom youtube-dl-process-model
'multiple-processes
84 "Determine youtube-dl.el downloading process model."
86 :type
'(choice (symbol :tag
"single process only" 'single-process
)
87 (symbol :tag
"multiple processes" 'multiple-processes
)))
89 (defcustom youtube-dl-max-failures
8
90 "Maximum number of retries for a single video."
95 (defcustom youtube-dl-slow-rate-limit
"2M"
96 "Download rate for slow item (corresponding to option --limit-rate)."
101 (defcustom youtube-dl-notify
(cl-case system-type
105 "Whether raise notifications."
110 (defcustom youtube-dl-destination-illegal-filename-chars-regex
"[#$%+!<>&*|{}?=@:/\\\"'`]"
111 "Specify the regex of invalid destination filename.
112 This will avoid youtube-dl can't save invalid filename caused error.
113 https://www.mtu.edu/umc/services/websites/writing/characters-avoid/"
119 ;; (replace-regexp-in-string
120 ;; youtube-dl-destination-illegal-filename-chars-regex "-"
121 ;; "[#$%+!<>&*|{}?=@:/\\\"'`]")
123 (defface youtube-dl-active
124 '((t :inherit font-lock-function-name-face
:foreground
"forest green"))
125 "Face for highlighting the active download item."
128 (defface youtube-dl-slow
129 '((t :inherit font-lock-variable-name-face
:foreground
"orange"))
130 "Face for highlighting the slow (S) tag."
133 (defface youtube-dl-pause
134 '((t :inherit font-lock-type-face
:foreground
"gray"))
135 "Face for highlighting the pause (P) tag."
138 (defface youtube-dl-priority
139 '((t :inherit font-lock-keyword-face
:foreground
"light blue"))
140 "Face for highlighting the priority marker."
143 (defface youtube-dl-failure
144 '((t :inherit font-lock-warning-face
:foreground
"red"))
145 "Face for highlighting the failure marker."
148 (defvar-local youtube-dl--log-item nil
149 "Item currently being displayed in the log buffer.")
151 (cl-defstruct (youtube-dl-item (:constructor youtube-dl-item--create
)
153 "Represents a single video to be downloaded with youtube-dl."
154 url
; Video URL (string)
155 vid
; Video ID (integer)
156 directory
; Working directory for youtube-dl (string or nil)
157 destination
; Preferred destination file (string or nil)
158 running
; Status indicator of process (boolean)
159 failures
; Number of video download failures (integer)
160 priority
; Download priority (integer)
161 title
; Listing display title (string or nil)
162 percentage
; Current download percentage (string or nil)
163 total-size
; Total download size (string or nil)
164 log
; All program output (list of strings)
165 log-end
; Last log item (list of strings)
166 paused-p
; Non-nil if download is paused (boolean)
167 slow-p
; Non-nil if download should be rate limited (boolean)
168 rate-limit
; Download rate limit (e.g. 50K or 2M)
169 buffer
; the process buffer name
170 process
; the process object
171 download-rate
; Download rate in bytes per second (e.g. 50K or 2M)
172 eta
; ETA time (e.g. 01:01:57)
175 (defvar youtube-dl-items
()
176 "List of all items still to be downloaded.")
178 (defvar youtube-dl-process nil
179 "The currently active youtube-dl process.")
181 (defun youtube-dl--process-buffer-name (vid)
182 "Return the process buffer name which constructed with VID."
183 (format " *youtube-dl vid:%s*" vid
))
185 (defun youtube-dl--proxy-append (url &optional option value
)
186 "Decide whether append proxy option in youtube-dl command based on URL."
187 (let ((domain (url-domain (url-generic-parse-url url
))))
188 (if (and (member domain youtube-dl-proxy-url-list
) ; <-- whether toggle proxy?
189 (not (string-empty-p youtube-dl-proxy
)))
190 (if option
; <-- whether has command-line option?
191 (list "--proxy" youtube-dl-proxy option value
)
192 (list "--proxy" youtube-dl-proxy
))
193 (if option
; <-- return original arguments for no proxy
195 nil
; <-- return nothing for url no need proxy
198 (defun youtube-dl--next ()
199 "Returns the next item to be downloaded."
200 (let (best best-score
)
201 (dolist (item youtube-dl-items best
)
202 (let* ((failures (youtube-dl-item-failures item
))
203 (priority (youtube-dl-item-priority item
))
204 (paused-p (youtube-dl-item-paused-p item
))
205 (score (- priority failures
)))
206 (when (and (not paused-p
)
207 (< failures youtube-dl-max-failures
))
211 ((> score best-score
)
213 best-score score
))))))))
215 (defun youtube-dl--current ()
216 "Return the item currently being downloaded."
217 (when youtube-dl-process
218 (plist-get (process-plist youtube-dl-process
) :item
)))
220 (defun youtube-dl--remove (item)
221 "Remove ITEM from the queue and kill process."
222 (let ((proc (youtube-dl-item-process item
)))
223 (when (process-live-p proc
) ; `kill-process' only when `proc' is still alive.
224 (kill-process proc
)))
225 (setf youtube-dl-items
(cl-delete item youtube-dl-items
)))
227 (defun youtube-dl--add (item)
228 "Add ITEM to the queue."
229 (setf youtube-dl-items
(nconc youtube-dl-items
(list item
))))
231 (defvar youtube-dl--timer-item nil
232 "A global variable for passing ITEM into `youtube-dl--run' in `run-with-timer'.")
234 (defun youtube-dl--sentinel (proc status
)
235 (when-let ((item (plist-get (process-plist proc
) :item
)))
236 ;; (setf youtube-dl-process proc) ; dont' need to set current process.
238 ;; process status is finished.
239 ((equal status
"finished\n")
240 ;; mark item structure property `:running' to `nil'.
241 (setf (youtube-dl-item-running item
) nil
)
242 (youtube-dl--remove item
)
243 (when youtube-dl-notify
244 (notifications-notify :title
"youtube-dl.el" :body
"Download finished.")))
245 ;; detect whether process is in "pause" process status?
246 ((youtube-dl-item-paused-p item
)
247 ;; mark item structure property `:running' to `nil'.
248 (setf (youtube-dl-item-running item
) nil
)
249 (message "[youtube-dl] process %s is paused." proc
))
250 ;; when process downloading failed, then retry downloading.
251 ((equal status
"killed: 9\n")
252 ;; mark item structure property `:running' to `nil'.
253 (setf (youtube-dl-item-running item
) nil
)
254 (message "[youtube-dl] process %s is killed." proc
))
256 ;; mark item structure property `:running' to `nil'.
257 (setf (youtube-dl-item-running item
) nil
)
258 (cl-incf (youtube-dl-item-failures item
))
259 ;; record log output to process buffer
260 (let ((buf (youtube-dl--log-buffer item
))
261 (inhibit-read-only t
))
262 (with-current-buffer buf
264 (print (process-plist proc
) buf
)))
266 (if (<= (youtube-dl-item-failures item
) 10)
268 (setq youtube-dl--timer-item item
)
269 (run-with-timer (* 60 5) nil
#'youtube-dl--run
)
270 (message (format "[youtube-dl] delay process %s" proc
)))))))
272 (defun youtube-dl--progress (output)
273 "Return the download progress for the given output.
274 Progress lines that straddle output chunks are lost. That's fine
275 since this is just used for display purposes.
277 Return list: (\"34.6%\" \"~2.61GiB\" \"929.50KiB/s\") "
281 ;; [download] 34.6% of ~2.61GiB at 929.50KiB/s ETA 01:01:57
282 ;; +---+ +------+ +---------+ +------+
283 ((string-equal youtube-dl-program
"youtube-dl")
284 (while (string-match "\\[download\\] +\\([^ ]+%\\) +of *~? *\\([^ ]+\\) +at +\\([^ ]+\\) +ETA +\\([^ \n]+\\)" output start
)
285 (let ((percent (match-string 1 output
))
286 (total-size (match-string 2 output
))
287 (download-rate (match-string 3 output
))
288 (eta (match-string 4 output
)))
289 (setf progress
(list percent total-size download-rate eta
)
290 start
(match-end 0)))))
291 ((string-equal youtube-dl-program
"yt-dlp")
292 ;; The output has non-ASCII shell color codes. Disable it with command-line option "--no-colors".
293 ;; [download] 0.4% of ~ 314.23MiB at 7.54KiB/s ETA 11:48:26 (frag 0/216)
294 ;; +--+ +-------+ +-------+ +------+
295 (while (string-match "\\[download\\] +\\([^ ]+%\\) +of *~? *\\([^ ]+\\) +at +\\([^ ]+\\) +ETA +\\([^ \n]+\\)" output start
)
296 (let ((percent (match-string 1 output
))
297 (total-size (match-string 2 output
))
298 (download-rate (match-string 3 output
))
299 (eta (match-string 4 output
)))
300 ;; filter out not correct matched data.
302 ;; (message "[DEBUG] [youtube-dl] download-rate regexp matched: >>> %s <<<" download-rate)
303 ;; (message "[DEBUG] [youtube-dl] eta regexp matched: >>> %s <<<" eta)
304 (unless (or (string-equal-ignore-case total-size
"Unknown.*")
305 (string-equal-ignore-case eta
"Unknown.*") ; [download] 0.0% of 2.14MiB at Unknown B/s ETA Unknown
307 (setf progress
(list percent total-size download-rate eta
)
308 start
(match-end 0)))))))
311 ;; (cl-destructuring-bind (percentage total-size download-rate eta) progress
312 ;; (message "[DEBUG] [youtube-dl] progress: percent: %s, total-size: %s, speed: %s, eta: %s."
313 ;; percentage total-size download-rate eta)))
316 (defun youtube-dl--get-destination (url)
317 "Return the destination filename for the given `URL' (if any).
318 The destination filename may potentially straddle two output
319 chunks, but this is incredibly unlikely. It's only used for
320 display purposes anyway.
322 $ youtube-dl --get-filename <URL>"
323 (message "[youtube-dl] getting video destination.")
324 (let* ((get-destination-buffer " *youtube-dl-get-destination*")
325 (get-destination-error-buffer " *youtube-dl-get-destination error*")
327 ;; clear destination buffer content to clear destination history.
329 (when (buffer-live-p (get-buffer get-destination-buffer
))
330 (with-current-buffer get-destination-buffer
(erase-buffer)))
331 (when (buffer-live-p (get-buffer get-destination-error-buffer
))
332 (with-current-buffer get-destination-error-buffer
(erase-buffer))))
333 ;; retrieve destination
335 :command
(list "youtube-dl" "--get-filename" url
)
336 :sentinel
(lambda (proc event
)
337 (if (string= event
"finished\n")
338 (setq destination
(replace-regexp-in-string
340 (with-current-buffer get-destination-buffer
(buffer-string)))))
341 (let* ((buffer-str (with-current-buffer get-destination-error-buffer
(buffer-string)))
342 (match-regex "\\(ERROR\\|Error\\): \\(.*\\)")
343 (error-p (string-match-p match-regex buffer-str
))
344 (matched-str (when (string-match match-regex buffer-str
) (match-string 2 buffer-str
))))
347 (user-error "[youtube-dl] `youtube-dl--get-destination' error!\n%s" matched-str
)
349 ;; (funcall 'youtube-dl--get-destination url)
352 (message "[youtube-dl] get output filename successed.")
353 ;; (kill-process proc)
354 ;; (kill-buffer (process-buffer proc))
356 :name
"youtube-dl-get-destination"
357 :buffer get-destination-buffer
358 :stderr get-destination-error-buffer
)
359 (setq destination-not-returned t
)
360 (while destination-not-returned
362 (if destination
(setq destination-not-returned nil
)))
363 (kill-new destination
) ; copy to Emacs kill-ring for later operation.
364 (message "[youtube-dl] destination: %s" destination
)
367 (defun youtube-dl--filter (proc output
)
368 (if (plist-get (process-plist proc
) :item
)
369 (let* ((item (plist-get (process-plist proc
) :item
))
370 (progress (youtube-dl--progress output
))
371 (destination (youtube-dl-item-destination item
)))
372 ;; Append to program log.
373 (let ((logged (list output
)))
374 (if (youtube-dl-item-log item
)
375 (setf (cdr (youtube-dl-item-log-end item
)) logged
376 (youtube-dl-item-log-end item
) logged
)
377 (setf (youtube-dl-item-log item
) logged
378 (youtube-dl-item-log-end item
) logged
)))
379 ;; Update progress information.
381 (cl-destructuring-bind (percentage total-size download-rate eta
) progress
382 (setf (youtube-dl-item-percentage item
) percentage
383 (youtube-dl-item-total-size item
) total-size
384 (youtube-dl-item-download-rate item
) download-rate
385 (youtube-dl-item-eta item
) eta
)))
386 ;; monitor process output whether error occured?
387 (let* ((item-log-end (youtube-dl-item-log-end item
))
389 ((stringp item-log-end
) item-log-end
)
390 ((listp item-log-end
) (car (last item-log-end
))))))
391 (if (and (stringp log-end-str
)
392 (or (string-match "ERROR:.*" log-end-str
)
393 (string-match "exited abnormally with code 1" log-end-str
)))
395 ;; mark item structure property `:running' to `nil'.
396 (setf (youtube-dl-item-running item
) nil
)
397 (message (format "[youtube-dl] process %s error!" (youtube-dl-item-process item
))))
398 (setf (youtube-dl-item-running item
) t
)))
400 ;; (message "[DEBUG] [youtube-dl] destination/title: %s" destination)
401 ;; Set item title to destination if it's empty.
402 (unless (youtube-dl-item-title item
)
403 (setf (youtube-dl-item-title item
) destination
))
404 ;; write output to process associated buffer.
405 (with-current-buffer (process-buffer proc
)
406 (let ((moving (= (point) (process-mark proc
)))
407 ;; avoid process buffer read-only issue for `insert'.
408 (inhibit-read-only t
))
410 ;; Insert the text, advancing the process marker.
411 (goto-char (process-mark proc
))
413 (set-marker (process-mark proc
) (point)))
414 (if moving
(goto-char (process-mark proc
)))))
415 (youtube-dl--redisplay))))
417 (defun youtube-dl--run-single-process ()
418 "Start youtube-dl downloading in single process."
419 (let ((item (youtube-dl--next))
420 (current-item (youtube-dl--current)))
421 (if (eq item current-item
)
422 (youtube-dl--redisplay) ; do nothing, just display the youtube-dl-list buffer.
423 (if youtube-dl-process
425 ;; Switch to higher priority job, but offset error count first.
426 (cl-decf (youtube-dl-item-failures current-item
))
427 (kill-process youtube-dl-process
)) ; sentinel will clean up
428 ;; No subprocess running, start a one.
429 (let* ((item-url (youtube-dl-item-url item
))
430 ;; fix link is an org-mode link is a property list.
431 (url (if (stringp item-url
) item-url
(substring-no-properties item-url
)))
432 (directory (youtube-dl-item-directory item
))
433 (destination (youtube-dl-item-destination item
))
434 (vid (youtube-dl-item-vid item
))
435 (proc-buffer-name (youtube-dl--process-buffer-name vid
))
438 (concat (directory-file-name directory
) "/")
439 (concat (directory-file-name youtube-dl-directory
) "/")))
440 (_ (mkdir default-directory t
))
441 (slow-p (youtube-dl-item-slow-p item
))
442 (rate-limit (or (youtube-dl-item-rate-limit item
) youtube-dl-slow-rate-limit
))
444 :name proc-buffer-name
445 :command
(let ((command (append (list youtube-dl-program
"--newline")
447 (cl-copy-list youtube-dl-arguments
)
448 ;; Disable non-ASCII shell color codes in output.
450 ((string-equal youtube-dl-program
"youtube-dl")
452 ((string-equal youtube-dl-program
"yt-dlp")
454 (youtube-dl--proxy-append url
)
456 `("--rate-limit" ,rate-limit
))
458 `("--output" ,destination
))
460 ;; Insert complete command into process buffer for debugging.
461 (with-current-buffer (get-buffer-create proc-buffer-name
)
462 (insert (format "Command: %s" command
)))
464 :sentinel
#'youtube-dl--sentinel
465 :filter
#'youtube-dl--filter
466 :buffer proc-buffer-name
)))
467 (set-process-plist proc
(list :item item
))
468 (setf youtube-dl-process proc
)
469 ;; mark item structure property `:running' to `t'.
470 (setf (youtube-dl-item-running item
) t
))))
471 (youtube-dl--redisplay)))
473 (defun youtube-dl--run-multiple-processes (&optional item
)
474 "Start youtube-dl downloading in multiple processes."
475 (let* ((item (or item
476 youtube-dl--timer-item
; use item from `run-with-timer'.
477 (with-current-buffer (youtube-dl--list-buffer)
478 (nth (1- (line-number-at-pos)) youtube-dl-items
))))
479 (current-item (youtube-dl--current)) ; `youtube-dl-process' currently actived process.
480 (proc (youtube-dl-item-process item
)))
481 (unless (and item
(youtube-dl-item-running item
)) ; whether item structure property `:running' is `t'?
482 (let* ((item-url (youtube-dl-item-url item
))
483 ;; fix link is an org-mode link is a property list.
484 (url (if (stringp item-url
) item-url
(substring-no-properties item-url
)))
485 (directory (youtube-dl-item-directory item
))
486 (destination (youtube-dl-item-destination item
))
487 (vid (youtube-dl-item-vid item
))
488 (proc-buffer-name (youtube-dl--process-buffer-name vid
))
491 (concat (directory-file-name directory
) "/")
492 (concat (directory-file-name youtube-dl-directory
) "/")))
493 (_ (mkdir default-directory t
))
494 (slow-p (youtube-dl-item-slow-p item
))
495 (rate-limit (or (youtube-dl-item-rate-limit item
) youtube-dl-slow-rate-limit
))
496 (failures (youtube-dl-item-failures item
))
497 (proc (when (<= failures
10) ; re-run process only when failures <= 10.
499 :name proc-buffer-name
500 :command
(let ((command (append (list youtube-dl-program
"--newline")
502 (cl-copy-list youtube-dl-arguments
)
503 ;; Disable non-ASCII shell color codes in output.
505 ((string-equal youtube-dl-program
"youtube-dl")
507 ((string-equal youtube-dl-program
"yt-dlp")
509 (youtube-dl--proxy-append url
)
511 `("--rate-limit" ,rate-limit
))
513 `("--output" ,destination
))
515 ;; Insert complete command into process buffer for debugging.
516 (with-current-buffer (get-buffer-create proc-buffer-name
)
517 (insert (format "Command: %s" command
)))
519 :sentinel
#'youtube-dl--sentinel
520 :filter
#'youtube-dl--filter
521 :buffer proc-buffer-name
))))
522 ;; clear temporary item variable `youtube-dl--timer-item'.
523 (setq youtube-dl--timer-item nil
)
524 (when (processp proc
)
525 ;; set process property list.
526 (set-process-plist proc
(list :item item
))
527 ;; assign `proc' object to item slot `:process'.
528 (setf (youtube-dl-item-process item
) proc
)
529 ;; set current youtube-dl process variable.
530 (setf youtube-dl-process proc
)
531 ;; mark item structure property `:running' to `t'.
532 (setf (youtube-dl-item-running item
) t
))))
533 (youtube-dl--redisplay)))
535 (defun youtube-dl--run (&optional item
)
536 "Start youtube-dl downloading."
537 ;; if single process model, then start download next item.
538 ;; if multiple processes model, then don't start next item `youtube-dl--run'.
539 (cl-case youtube-dl-process-model
540 (single-process (youtube-dl--run-single-process item
))
541 (multiple-processes (youtube-dl--run-multiple-processes item
))))
543 (defun youtube-dl--get-vid (url)
544 "Get video `URL' video vid with youtube-dl option `--get-id'."
545 (message "[youtube-dl] getting video vid.")
546 (let* ((parsed-url (url-generic-parse-url url
))
547 (domain (url-domain parsed-url
))
548 (parameters (url-filename parsed-url
)))
549 ;; URL domain matching with regexp to extract vid.
551 ("bilibili.com" ; "/video/BV1B8411L7te/", "/video/BV1uj411N7cp?spm_id_from=..0.0"
552 (when (string-match "/video/\\([^/?&]*\\)" parameters
)
553 (match-string 1 parameters
)))
554 ("pornhub.com" ; "/view_video.php?viewkey=ph6238f9c7cc9e2"
555 (when (string-match "/view_video\\.php\\?viewkey=\\([^/?&]*\\)" parameters
)
556 (match-string 1 parameters
)))
557 ("youtube.com" ; "/watch?v=q-iLEteyeUQ", "/watch?v=48JlgiBpw_I&t=311s"
558 (when (string-match "/watch\\?v=\\([^/?&]*\\)" parameters
)
559 (match-string 1 parameters
)))
561 (let* ((output (with-temp-buffer
562 (apply #'call-process
565 (youtube-dl--proxy-append url
"--get-id" url
))
567 (output-lines-list (string-lines output
))
568 (error-p (string-match-p "ERROR" output
)))
570 ;; ("VrAfJvZGGXE" "")
571 ;; TEST: (youtube-dl--get-vid "https://www.youtube.com/watch?v=VROjLiq9LeQ")
572 ((length= output-lines-list
2) (car output-lines-list
))
573 ;; TEST: (youtube-dl--get-vid "https://hanime1.me/watch?v=22454")
574 ;; ("WARNING: Falling back on generic information extractor." "watch?v=22454" "")
575 ((length= output-lines-list
3) (car (last output-lines-list
1)))
576 ;; TEST: (youtube-dl--get-vid "https://www.pornhub.com/view_video.php?viewkey=ph637ed6daf1795")
577 ;; WARNING: unable to extract view count; please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see https://yt-dl.org/update on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.
578 ((string-match-p "WARNING" (car output-lines-list
))
579 (car (last output-lines-list
1)))
580 ;; get vid failed, use URL string regex matching instead.
583 ;; WARNING: Could not send HEAD request to https://hanime1.me/watch?v=22709:
584 ;; HTTP Error 503: Service Temporarily Unavailable
585 ;; ERROR: Unable to download webpage: HTTP Error 503: Service Temporarily
586 ;; Unavailable (caused by <HTTPError 503: 'Service Temporarily
587 ;; Unavailable'>); please report this issue on https://yt-dl.org/bug . Make
588 ;; sure you are using the latest version; see https://yt-dl.org/update on
589 ;; how to update. Be sure to call youtube-dl with the --verbose flag and
590 ;; include its complete output.
592 ;; WARNING: unable to extract view count; please report this
593 ;; issue on https://yt-dl.org/bug . Make sure you are using
594 ;; the latest version; see https://yt-dl.org/update on how
595 ;; to update. Be sure to call youtube-dl with the --verbose
596 ;; flag and include its complete output.
597 (error (format "[youtube-dl] `youtube-dl--get-vid' retrive video vid error!\n%s" output
)))
601 ;; (let ((parameters "/video/BV1B8411L7te/"))
602 ;; (when (string-match "/video/\\([^/]*\\)" parameters)
603 ;; (match-string 1 parameters)))
605 ;; (youtube-dl--get-vid "https://www.youtube.com/watch?v=VROjLiq9LeQ")
606 ;; (youtube-dl--get-vid "https://www.pornhub.com/view_video.php?viewkey=ph637ed6daf1795")
610 (url &key title
(priority 0) directory destination paused slow
)
611 "Queues URL for download using youtube-dl, returning the new item.
612 By default, it downloads to ~/Downloads/."
614 (list (substring-no-properties
615 (read-from-minibuffer
616 "URL: " (or (thing-at-point 'url
)
617 (when interprogram-paste-function
618 (funcall interprogram-paste-function
)))))))
619 ;; remove this ID failure only on youtube.com, use URL as ID. or use youtube-dl extracted title, or hash on URL.
620 (let* ((vid (youtube-dl--get-vid url
))
621 (destination (replace-regexp-in-string ; replace illegal filename characters in `destination'.
622 youtube-dl-destination-illegal-filename-chars-regex
"-"
623 (or (youtube-dl--get-destination url
) destination
)))
624 (title (or (replace-regexp-in-string (format "-%s.*" vid
) "" destination
)
625 (car (split-string destination
"-"))))
626 (proc-buffer-name (youtube-dl--process-buffer-name vid
))
627 (full-dir (expand-file-name (or directory
"") youtube-dl-directory
))
628 (item (youtube-dl-item--create :url url
636 :destination destination
638 :buffer proc-buffer-name
641 (unless (youtube-dl-item-running item
)
642 (youtube-dl--add item
) ; Add ITEM to the queue.
643 (youtube-dl--run item
) ; Start ITEM in the queue.
644 (when youtube-dl-auto-show-list
645 (youtube-dl-list))))))
647 (defalias 'youtube-dl-download-video
'youtube-dl
)
649 (defun youtube-dl--playlist-list (playlist)
650 "For each video, return one plist with :index, :vid, and :title."
652 (when (zerop (call-process youtube-dl-program nil t nil
657 (goto-char (point-min))
658 (cl-loop with json-object-type
= 'plist
660 for video
= (ignore-errors (json-read))
662 collect
(list :index index
663 :vid
(plist-get video
:vid
)
664 :title
(plist-get video
:title
))))))
666 (defun youtube-dl--playlist-reverse (list)
667 "Return a copy of LIST with the indexes reversed."
668 (let ((max (cl-loop for entry in list
669 maximize
(plist-get entry
:index
))))
670 (cl-loop for entry in list
671 for index
= (plist-get entry
:index
)
672 for copy
= (copy-sequence entry
)
673 collect
(plist-put copy
:index
(- (1+ max
) index
)))))
675 (defun youtube-dl--playlist-cutoff (list n
)
676 "Return a sorted copy of LIST with all items except where :index < N."
677 (let ((key (lambda (v) (plist-get v
:index
)))
678 (filter (lambda (v) (< (plist-get v
:index
) n
)))
679 (copy (copy-sequence list
)))
680 (cl-delete-if filter
(cl-stable-sort copy
#'< :key key
))))
683 (cl-defun youtube-dl-download-playlist
684 (url &key directory
(first 1) paused
(priority 0) reverse slow
)
685 "Add entire playlist to download queue, with index prefixes.
687 :directory PATH -- Destination directory for all videos.
689 :first INDEX -- Start downloading from a given one-based index.
691 :paused BOOL -- Start all download entries as paused.
693 :priority PRIORITY -- Use this priority for all download entries.
695 :reverse BOOL -- Reverse the video numbering, solving the problem
696 of reversed playlists.
698 :slow BOOL -- Start all download entries in slow mode."
700 (list (read-from-minibuffer
702 (when interprogram-paste-function
703 (funcall interprogram-paste-function
)))))
704 (message "[youtube-dl] fetching playlist ...")
705 (let ((videos (youtube-dl--playlist-list url
)))
707 (error "Failed to fetch playlist (%s)." url
)
708 (let* ((max (cl-loop for entry in videos
709 maximize
(plist-get entry
:index
)))
710 (width (1+ (floor (log max
10))))
711 (prefix-format (format "%%0%dd" width
)))
713 (setf videos
(youtube-dl--playlist-reverse videos
)))
714 (dolist (video (youtube-dl--playlist-cutoff videos first
))
715 (let* ((index (plist-get video
:index
))
716 (prefix (format prefix-format index
))
717 (title (format "%s-%s" prefix
(plist-get video
:title
)))
718 (dest (format "%s-%s" prefix
"%(title)s-%(id)s.%(ext)s")))
719 (youtube-dl (plist-get video
:url
)
727 ;; List user interface:
729 ;;; refresh youtube-dl-list buffer (2).
730 (defun youtube-dl-list-redisplay ()
731 "Immediately redraw the queue list buffer."
733 (with-current-buffer (youtube-dl--list-buffer)
734 (let ((save-point (point))
735 (window (get-buffer-window (current-buffer))))
736 (youtube-dl--fill-listing)
737 (goto-char save-point
)
739 (set-window-point window save-point
))
741 (hl-line-highlight)))))
743 ;;; refresh youtube-dl-list buffer (1).
744 (defun youtube-dl--redisplay ()
745 "Redraw the queue list buffer only if visible."
746 (let ((log-buffer (youtube-dl--log-buffer)))
748 (with-current-buffer log-buffer
749 (let ((inhibit-read-only t
)
750 (window (get-buffer-window log-buffer
)))
752 (mapc #'insert
(youtube-dl-item-log youtube-dl--log-item
))
754 (set-window-point window
(point-max)))))))
755 (when (get-buffer-window (youtube-dl--list-buffer))
756 (youtube-dl-list-redisplay)))
758 (defun youtube-dl-list-log ()
759 "Display the log of the video under point."
761 (let* ((n (1- (line-number-at-pos)))
762 (item (nth n youtube-dl-items
))
763 (buffer (youtube-dl--log-buffer item
)))
765 (display-buffer buffer
)
766 (select-window (get-buffer-window buffer
))
767 (youtube-dl--redisplay))))
769 (defun youtube-dl-list-kill-log ()
770 "Kill the youtube-dl log buffer."
772 (let ((buffer (youtube-dl--log-buffer)))
774 (kill-buffer buffer
))))
776 (defun youtube-dl-list-yank ()
777 "Copy the URL of the video under point to the clipboard."
779 (let* ((n (1- (line-number-at-pos)))
780 (item (nth n youtube-dl-items
)))
782 (let ((url (concat "https://youtu.be/" (youtube-dl-item-vid item
))))
783 (if (fboundp 'gui-set-selection
)
784 (gui-set-selection nil url
) ; >= Emacs 25
786 (x-set-selection 'PRIMARY url
))) ; <= Emacs 24
787 (message "[youtube-dl] yanked %s" url
)))))
789 (defun youtube-dl-list-kill ()
790 "Remove the selected item from the queue."
792 (when youtube-dl-items
; avoid `youtube-dl-items' is `nil'.
793 (let* ((n (1- (line-number-at-pos)))
794 (item (nth n youtube-dl-items
)))
796 (when (= n
(1- (length youtube-dl-items
)))
798 (youtube-dl--remove item
))))
799 (youtube-dl-list-redisplay))
801 (defun youtube-dl-list-pause ()
802 "Pause downloading of item under point."
804 (let* ((n (1- (line-number-at-pos)))
805 (item (nth n youtube-dl-items
))
806 (proc (youtube-dl-item-process item
))
807 (paused-p (youtube-dl-item-paused-p item
)))
808 (if (and item paused-p
)
809 ;; if paused, resume process.
811 (setf (youtube-dl-item-paused-p item
) nil
)
813 ;; kill the process, but keep item on list buffer for pause status.
814 (when (process-live-p proc
) (kill-process proc
))
815 ;; after killed process, also setting item structure status properties.
816 (setf (youtube-dl-item-paused-p item
) t
)
817 (setf (youtube-dl-item-running item
) nil
))
818 (youtube-dl-list-redisplay)
819 (message "[youtube-dl] process %s is paused." proc
)))
821 (defun youtube-dl-list-resume ()
822 "Resume failure/paused downloading item under point."
824 (when-let* ((n (1- (line-number-at-pos)))
825 (item (nth n youtube-dl-items
))
826 (proc (youtube-dl-item-process item
)))
828 (setf (youtube-dl-item-failures item
) 0)
830 ((youtube-dl-item-paused-p item
)
831 (setf (youtube-dl-item-paused-p item
) nil
)
833 ((not (youtube-dl-item-running item
))
836 (setf (youtube-dl-item-running item
) nil
)
838 ;; (user-error (format "[youtube-dl] Can't resume process %s correctly. Try resume again." proc))
840 (youtube-dl-list-redisplay))
842 (defun youtube-dl-list-toggle-slow (item &optional rate-limit
)
843 "Set slow rate limit on item under point."
845 (let* ((n (1- (line-number-at-pos))))
846 (list (nth n youtube-dl-items
))))
848 (let ((proc (youtube-dl-item-process item
))
849 (slow-p (youtube-dl-item-slow-p item
))
850 (rate-limit (substring-no-properties
852 (completing-read "[youtube-dl] limit download rate (e.g. 100K): "
853 '("20K" "50K" "100K" "200K" "500K" "1M" "2M"))))))
854 ;; restart with a slower download rate.
855 (when (process-live-p proc
) (kill-process proc
))
856 (setf (youtube-dl-item-slow-p item
) (not slow-p
))
857 (setf (youtube-dl-item-rate-limit item
) rate-limit
)
859 (youtube-dl-list-redisplay))
861 (defun youtube-dl-list-toggle-slow-all ()
862 "Set slow rate on all items."
864 (let* ((count (length youtube-dl-items
))
865 (slow-count (cl-count-if #'youtube-dl-item-slow-p youtube-dl-items
))
866 (target (< slow-count
(- count slow-count
)))
867 (rate-limit (completing-read "[youtube-dl] limit download rate (e.g. 100K): "
868 '("20K" "50K" "100K" "200K" "500K" "1M" "2M"))))
869 (dolist (item youtube-dl-items
)
870 (youtube-dl-list-toggle-slow item rate-limit
)))
871 (youtube-dl--redisplay))
873 (defun youtube-dl-list-priority-modify (delta)
874 "Change priority of item under point by DELTA."
875 (let* ((n (1- (line-number-at-pos)))
876 (item (nth n youtube-dl-items
)))
878 (cl-incf (youtube-dl-item-priority item
) delta
)
881 (defun youtube-dl-list-priority-up ()
882 "Decrease priority of item under point."
884 (youtube-dl-list-priority-modify 1))
886 (defun youtube-dl-list-priority-down ()
887 "Increase priority of item under point."
889 (youtube-dl-list-priority-modify -
1))
891 (defvar youtube-dl-list-mode-map
892 (let ((map (make-sparse-keymap)))
894 (define-key map
"a" #'youtube-dl
)
895 (define-key map
"g" #'youtube-dl-list-redisplay
)
896 (define-key map
"l" #'youtube-dl-list-log
)
897 (define-key map
"L" #'youtube-dl-list-kill-log
)
898 (define-key map
"y" #'youtube-dl-list-yank
)
899 (define-key map
"k" #'youtube-dl-list-kill
)
900 (define-key map
"p" #'youtube-dl-list-pause
)
901 (define-key map
"r" #'youtube-dl-list-resume
)
902 (define-key map
"s" #'youtube-dl-list-toggle-slow
)
903 (define-key map
"S" #'youtube-dl-list-toggle-slow-all
)
904 (define-key map
"]" #'youtube-dl-list-priority-up
)
905 (define-key map
"[" #'youtube-dl-list-priority-down
)))
906 "Keymap for `youtube-dl-list-mode'")
908 (defvar-local youtube-dl-list--auto-close-window-timer nil
909 "A timer to auto close youtube-dl list window.")
911 (defun youtube-dl-list--auto-close-window ()
912 "Auto close '*youtube-dl list*' buffer after finished all downloading."
913 (when (equal (length youtube-dl-items
) 0)
914 (when-let ((buf (get-buffer-window (youtube-dl--list-buffer))))
916 (when (timerp youtube-dl-list--auto-close-window-timer
)
917 (cancel-timer youtube-dl-list--auto-close-window-timer
)))))
919 (defvar youtube-dl-list--format
920 ;; (percentage download-rate)
922 ;;space,vid,progress size eta fails
924 ;;| | | | | | | title
926 "%s%-16s %-22.22s %-10.10s %-6.6s %-7.7s %-8.8s %s"
927 "Define the `youtube-dl-list-mode' `header-line-format' and list item format.")
929 (define-derived-mode youtube-dl-list-mode special-mode
"youtube-dl"
930 "Major mode for listing the youtube-dl download queue."
932 (use-local-map youtube-dl-list-mode-map
)
934 (setq-local truncate-lines t
) ; truncate long line text.
935 (setf header-line-format
937 (format youtube-dl-list--format
938 (propertize " " 'display
'((space :align-to
0))) ; space
939 " vid " "| progress" "| size" "| ETA" "| fails" "| status"
940 (concat "| title " (make-string 100 (string-to-char " "))))
941 'face
'(:inverse-video t
:extend t
))))
943 (defvar youtube-dl--list-buffer-name
" *youtube-dl list*"
944 "The buffer name of `youtube-dl-list-mode'.")
946 (defun youtube-dl--list-buffer ()
947 "Returns the queue listing buffer."
948 (if-let ((buf (get-buffer-create youtube-dl--list-buffer-name
)))
949 (with-current-buffer buf
950 ;; TODO: use `tabulated-list-mode'.
951 ;; (tabulated-list-mode)
952 (youtube-dl-list-mode)
955 (defun youtube-dl--log-buffer (&optional item
)
956 "Returns a youtube-dl log buffer for ITEM."
958 (let* ((name (youtube-dl-item-buffer item
))
959 (buffer (get-buffer-create name
)))
960 (with-current-buffer buffer
961 (unless (eq major-mode
'special-mode
)
963 (setf youtube-dl--log-item item
)
964 (setf (youtube-dl-item-log item
) item
)
967 ;;; refresh youtube-dl-list buffer (3).
968 (defun youtube-dl--fill-listing ()
969 "Erase and redraw the queue in the queue listing buffer."
970 (with-current-buffer (youtube-dl--list-buffer)
971 (let* ((inhibit-read-only t
)
972 (active (youtube-dl--current)))
974 (dolist (item youtube-dl-items
)
975 (let ((vid (youtube-dl-item-vid item
))
976 (running (youtube-dl-item-running item
))
977 (failures (youtube-dl-item-failures item
))
978 (priority (youtube-dl-item-priority item
))
979 (percentage (youtube-dl-item-percentage item
))
980 (download-rate (youtube-dl-item-download-rate item
))
981 (paused-p (youtube-dl-item-paused-p item
))
982 (slow-p (youtube-dl-item-slow-p item
))
983 (rate-limit (youtube-dl-item-rate-limit item
))
984 (total-size (youtube-dl-item-total-size item
))
985 (eta (youtube-dl-item-eta item
))
986 (title (youtube-dl-item-title item
))
987 (url (youtube-dl-item-url item
)))
990 (format (concat youtube-dl-list--format
"\n")
993 ;; (propertize " " 'display '((space :align-to 0)))
995 (if running
; update `:running' property every time process update.
996 (propertize vid
'face
'default
)
997 (propertize vid
'face
'youtube-dl-pause
))
998 ;; progress (percentage download-rate)
999 (if (and percentage download-rate
)
1000 (propertize (format "⦼%-5.5s ⇩%-17.17s " percentage download-rate
) 'face
'youtube-dl-active
)
1001 (propertize (format "⦼%-5.5s ⇩%-17.17s " percentage download-rate
) 'face
'youtube-dl-pause
))
1003 (or (propertize (format "%s" total-size
) 'face
'youtube-dl-pause
) "-")
1005 (or (propertize (format "%s" eta
) 'face
'youtube-dl-pause
) "?")
1009 (propertize (format " [%d] " failures
) 'face
'youtube-dl-failure
))
1011 ;; (if (= priority 0)
1013 ;; (propertize (format "%+d " priority) 'face 'youtube-dl-priority))
1016 (if slow-p
(propertize "SLOW" 'face
'youtube-dl-slow
))
1017 (if rate-limit
(propertize (format "≤ %s" rate-limit
) 'face
'youtube-dl-slow
))
1018 (if paused-p
(propertize "PAUSE" 'face
'youtube-dl-pause
)))
1024 (defun youtube-dl-list ()
1025 "Display a list of all videos queued for download."
1027 (youtube-dl--fill-listing)
1028 ;; set timer to auto close `youtube-dl--list-buffer' "*youtube-dl list*" buffer after finished all downloading.
1029 (with-current-buffer (youtube-dl--list-buffer)
1030 (unless (timerp youtube-dl-list--auto-close-window-timer
)
1031 (setq-local youtube-dl-list--auto-close-window-timer
1032 (run-with-timer 0 (* 60 2) #'youtube-dl-list--auto-close-window
)))
1033 ;; jump to beginning of buffer.
1034 (goto-char (point-min)))
1035 (display-buffer (youtube-dl--list-buffer)))
1038 (defun youtube-dl-download-audio (url)
1039 "Download audio format of URL."
1041 (list (read-from-minibuffer
1042 "URL: " (or (thing-at-point 'url
)
1043 (when interprogram-paste-function
1044 (funcall interprogram-paste-function
))))))
1045 (let ((youtube-dl-arguments (append youtube-dl-arguments
'("-x" "--audio-format" "best"))))
1049 (defun youtube-dl-download-subtitle (url)
1050 "Download video subtitle of URL."
1052 (list (read-from-minibuffer
1053 "URL: " (or (thing-at-point 'url
)
1054 (when interprogram-paste-function
1055 (funcall interprogram-paste-function
))))))
1056 (let ((youtube-dl-arguments (append youtube-dl-arguments
1057 '("--skip-download" "--no-warnings"
1058 ;; "--write-auto-sub" ; YouTube only, for auto-generated subtitle.
1060 "--sub-lang" "en,zh-Hans"
1061 "--sub-format" "ass/srt/best" ; ass/srt/best, srt, vtt, ttml, srv3, srv2, srv1
1066 (defun youtube-dl-download-thumbnail (url)
1067 "Download video thumbnail of URL."
1069 (list (read-from-minibuffer
1070 "URL: " (or (thing-at-point 'url
)
1071 (when interprogram-paste-function
1072 (funcall interprogram-paste-function
))))))
1073 (let ((youtube-dl-arguments (append youtube-dl-arguments
1074 '("--skip-download" "--no-warnings"
1075 "--write-thumbnail"))))
1078 (provide 'youtube-dl
)
1080 ;;; youtube-dl.el ends here