improve directory `completing-read` asking
[youtube-dl.el.git] / youtube-dl.el
blob2f4371c802460779a3d40d0bc54bfed44456d4e6
1 ;;; youtube-dl.el --- Manages a youtube-dl queue -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2021 stardiviner <numbchild@gmail.com>
5 ;; Original Author: Christopher Wellons <wellons@nullprogram.com>
6 ;; URL: https://github.com/skeeto/youtube-dl-emacs
7 ;; Version: 1.0
8 ;; Package-Requires: ((emacs "29.1") (transient "0.4.0") (s "1.13.1"))
10 ;; This file is not part of GNU Emacs.
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; any later version.
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program; if not, you can either send email to this
24 ;; program's maintainer or write to: The Free Software Foundation,
25 ;; Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA.
27 ;;; Commentary:
29 ;; This package manages a video download queue for the youtube-dl
30 ;; command line program, which serves as its back end. It manages a
31 ;; single youtube-dl subprocess to download one video at a time. New
32 ;; videos can be queued at any time.
34 ;; The `youtube-dl' command queues a URL for download. Failures are
35 ;; retried up to `youtube-dl-max-failures'. Items can be paused or set
36 ;; to be downloaded at a slower download rate (`youtube-dl-slow-rate-limit').
38 ;; The `youtube-dl-download-playlist' command queues an entire playlist, just
39 ;; as if you had individually queued each video on the playlist.
41 ;; The `youtube-dl-list' command displays a list of all active video
42 ;; downloads. From this list, items under point can be canceled (d),
43 ;; paused (p), slowed (s), and have its priority adjusted ([ and ]).
45 ;;; Code:
47 (require 'json)
48 (require 'cl-lib)
49 (require 'hl-line)
50 (require 'notifications)
51 (require 'org-element)
52 (require 'org-attach)
53 (require 's) ; for `s-truncate'
56 (defgroup youtube-dl ()
57 "Download queue for the `youtube-dl' command line program."
58 :group 'external)
60 (defcustom youtube-dl-download-directory "~/Downloads/"
61 "Directory in which to run `youtube-dl'."
62 :group 'youtube-dl
63 :type 'directory)
65 (defcustom youtube-dl-program
66 ;; "yt-dlp" is a fork of "youtube-dl".
67 (let ((program (cond
68 ((executable-find "yt-dlp") "yt-dlp")
69 ((executable-find "youtube-dl") "youtube-dl"))))
70 (if (or (file-exists-p program) (file-regular-p program))
71 (file-name-base program)
72 program))
73 "The name of the program invoked for downloading YouTube videos.
74 NOTE: It's specified by program name instead of program path."
75 :type 'string
76 :safe #'stringp
77 :group 'youtube-dl)
79 (defcustom youtube-dl-extra-arguments
80 '("--newline" "--no-warnings" "--embed-metadata")
81 "Arguments to be send to `youtube-dl'.
82 Instead of --limit-rate use custom option `youtube-dl-slow-rate-limit'."
83 :type '(repeat string)
84 :safe #'listp
85 :group 'youtube-dl)
87 (defcustom youtube-dl-omit-mtime t
88 "Whether to omit timestamp from the `Last-modified' header for downloaded files."
89 :type 'boolean
90 :safe #'booleanp
91 :group 'youtube-dl)
93 (defcustom youtube-dl-restrict-filenames t
94 "Whether to restrict downloaded filenames to only ASCII characters."
95 :type 'boolean
96 :safe #'booleanp
97 :group 'youtube-dl)
99 (defcustom youtube-dl-proxy ""
100 "Specify the proxy for `youtube-dl' command.
101 For example:
103 127.0.0.1:8118
104 socks5://127.0.0.1:1086"
105 :type 'string
106 :safe #'stringp
107 :group 'youtube-dl)
109 (defcustom youtube-dl-proxy-url-list '()
110 "A list of URL domains which should use proxy for `youtube-dl'."
111 :type 'list
112 :safe #'listp
113 :group 'youtube-dl)
115 (defcustom youtube-dl-auto-show-list t
116 "Auto show the \"*youtube-dl list*\" buffer."
117 :type 'boolean
118 :safe #'booleanp
119 :group 'youtube-dl)
121 (defcustom youtube-dl-process-model 'multiple-processes
122 "Determine youtube-dl.el downloading process model."
123 :safe #'symbolp
124 :type '(choice (symbol :tag "single process only" 'single-process)
125 (symbol :tag "multiple processes" 'multiple-processes)))
127 (defcustom youtube-dl-max-failures 8
128 "Maximum number of retries for a single video."
129 :type 'integer
130 :safe #'numberp
131 :group 'youtube-dl)
133 (defcustom youtube-dl-slow-rate-limit "2M"
134 "Download rate for slow item (corresponding to option --limit-rate)."
135 :type 'string
136 :safe #'stringp
137 :group 'youtube-dl)
139 (defcustom youtube-dl-notify nil
140 "Whether raise notifications."
141 :type 'boolean
142 :safe #'booleanp
143 :group 'youtube-dl)
145 (defcustom youtube-dl-destination-illegal-filename-chars-regex "[#$%+!<>&*|{}?=@:/\\\"'`]"
146 "Specify the regex of invalid destination filename.
147 This will avoid `youtube-dl' can't save invalid filename caused error.
148 https://www.mtu.edu/umc/services/websites/writing/characters-avoid/"
149 :type 'string
150 :safe #'stringp
151 :group 'youtube-dl)
153 (defcustom youtube-dl-auto-close-list-buffer nil
154 "Non-nil means auto close the \"*youtube-dl list*\" buffer."
155 :type 'boolean
156 :safe #'booleanp
157 :group 'youtube-dl)
159 ;;; TEST:
160 ;; (replace-regexp-in-string
161 ;; youtube-dl-destination-illegal-filename-chars-regex "-"
162 ;; "[#$%+!<>&*|{}?=@:/\\\"'`]")
164 (defface youtube-dl-active
165 '((t :inherit font-lock-function-name-face :foreground "forest green"))
166 "Face for highlighting the active download item."
167 :group 'youtube-dl)
169 (defface youtube-dl-slow
170 '((t :inherit font-lock-variable-name-face :foreground "orange"))
171 "Face for highlighting the slow (S) tag."
172 :group 'youtube-dl)
174 (defface youtube-dl-pause
175 '((t :inherit font-lock-type-face :foreground "gray"))
176 "Face for highlighting the pause (P) tag."
177 :group 'youtube-dl)
179 (defface youtube-dl-priority
180 '((t :inherit font-lock-keyword-face :foreground "light blue"))
181 "Face for highlighting the priority marker."
182 :group 'youtube-dl)
184 (defface youtube-dl-failure
185 '((t :inherit font-lock-warning-face :foreground "red"))
186 "Face for highlighting the failure marker."
187 :group 'youtube-dl)
189 (defvar-local youtube-dl--log-item nil
190 "Item currently being displayed in the log buffer.")
192 (cl-defstruct (youtube-dl-item (:constructor youtube-dl-item--create)
193 (:copier nil))
194 "Represents a single video to be downloaded with youtube-dl."
195 url ; Video URL (string)
196 vid ; Video ID (integer)
197 directory ; Working directory for youtube-dl (string or nil)
198 destination ; Preferred destination file (string or nil)
199 running ; Status indicator of process (boolean)
200 failures ; Number of video download failures (integer)
201 priority ; Download priority (integer)
202 title ; Listing display title (string or nil)
203 percentage ; Current download percentage (string or nil)
204 total-size ; Total download size (string or nil)
205 log ; All program output (list of strings)
206 log-end ; Last log item (list of strings)
207 paused-p ; Non-nil if download is paused (boolean)
208 slow-p ; Non-nil if download should be rate limited (boolean)
209 rate-limit ; Download rate limit (e.g. 50K or 2M)
210 buffer ; the process buffer name
211 process ; the process object
212 finished-p ; Non-nil if the process is finished (boolean)
213 download-rate ; Download rate in bytes per second (e.g. 50K or 2M)
214 eta ; ETA time (e.g. 01:01:57)
215 type ; media type: video, audio, subtitle, thumbnail etc.
218 (defvar youtube-dl-items ()
219 "List of all items still to be downloaded.")
221 (defvar youtube-dl-process nil
222 "The currently active `youtube-dl' process.")
224 (defun youtube-dl--process-buffer-name (vid)
225 "Return the process buffer name which constructed with VID."
226 (format " *youtube-dl vid:%s*" vid))
228 (defun youtube-dl--proxy-append (url &optional option value)
229 "Decide whether append proxy OPTION & VALUE in `youtube-dl' command based on URL."
230 (let ((domain (url-domain (url-generic-parse-url url))))
231 (if (and (member domain youtube-dl-proxy-url-list) ; <-- whether toggle proxy?
232 (not (string-empty-p youtube-dl-proxy)))
233 (if option ; <-- whether has command-line option?
234 (list "--proxy" youtube-dl-proxy option value)
235 (list "--proxy" youtube-dl-proxy))
236 (if option ; <-- return original arguments for no proxy
237 (list option value)
238 nil ; <-- return nothing for url no need proxy
239 ))))
241 (defun youtube-dl--next ()
242 "Returns the next item to be downloaded."
243 (let (best best-score)
244 (dolist (item youtube-dl-items best)
245 (let* ((failures (youtube-dl-item-failures item))
246 (priority (youtube-dl-item-priority item))
247 (paused-p (youtube-dl-item-paused-p item))
248 (score (- priority failures)))
249 (when (and (not paused-p)
250 (< failures youtube-dl-max-failures))
251 (cond ((null best)
252 (setf best item
253 best-score score))
254 ((> score best-score)
255 (setf best item
256 best-score score))))))))
258 (defun youtube-dl--current ()
259 "Return the item currently being downloaded."
260 (when youtube-dl-process
261 (plist-get (process-plist youtube-dl-process) :item)))
263 (defun youtube-dl--remove (item)
264 "Remove ITEM from the queue and kill process."
265 (let ((proc (youtube-dl-item-process item)))
266 (when (process-live-p proc) ; `kill-process' only when `proc' is still alive.
267 (kill-process proc)))
268 (setf youtube-dl-items (cl-delete item youtube-dl-items)))
270 (defun youtube-dl--add (item)
271 "Add ITEM to the queue."
272 (setf youtube-dl-items (nconc youtube-dl-items (list item))))
274 (defvar youtube-dl--timer-item nil
275 "A global variable for passing ITEM into `youtube-dl--run' in `run-with-timer'.")
277 (defun youtube-dl-notify ()
278 "Raise a system notify and play a notify sound."
279 (cl-case system-type
280 (gnu/linux
281 (notifications-notify :title "youtube-dl.el" :body "Download finished."))
282 (darwin
283 (ns-do-applescript
284 (format "display notification \"%s\" with title \"%s\" sound name \"Blow\"" "youtube-dl.el" "Download finished.")))
285 (windows-nt nil)))
287 (defun youtube-dl--sentinel (proc status)
288 "The sentinel function with arguments of process PROC and STATUS."
289 (when-let ((item (plist-get (process-plist proc) :item)))
290 ;; (setf youtube-dl-process proc) ; dont' need to set current process.
291 (cond
292 ;; process status is finished.
293 ((equal status "finished\n")
294 ;; mark item structure property `:running' to `nil'.
295 (setf (youtube-dl-item-running item) nil)
296 (setf (youtube-dl-item-finished-p item) t)
297 (youtube-dl--remove item)
298 (when youtube-dl-notify
299 (youtube-dl-notify)))
300 ;; detect whether process is in "pause" process status?
301 ((youtube-dl-item-paused-p item)
302 ;; mark item structure property `:running' to `nil'.
303 (setf (youtube-dl-item-running item) nil)
304 (message "[youtube-dl] process %s is paused." proc))
305 ;; when process downloading failed, then retry downloading.
306 ((equal status "killed: 9\n")
307 ;; mark item structure property `:running' to `nil'.
308 (setf (youtube-dl-item-running item) nil)
309 (message "[youtube-dl] process %s is killed." proc))
311 ;; mark item structure property `:running' to `nil'.
312 (setf (youtube-dl-item-running item) nil)
313 (cl-incf (youtube-dl-item-failures item))
314 ;; record log output to process buffer
315 (let ((buf (youtube-dl--log-buffer item))
316 (inhibit-read-only t))
317 (with-current-buffer buf
318 (print status buf)
319 (print (process-plist proc) buf)))
320 ;; re-run process
321 (if (<= (youtube-dl-item-failures item) 10)
322 (youtube-dl--run)
323 (setq youtube-dl--timer-item item)
324 (run-with-timer (* 60 5) nil #'youtube-dl--run)
325 (message "[youtube-dl] delay process %s" proc))))))
327 (defun youtube-dl--progress (output)
328 "Return the download progress for the given OUTPUT.
329 Progress lines that straddle output chunks are lost. That's fine
330 since this is just used for display purposes.
332 Return list: (\"34.6%\" \"~2.61GiB\" \"929.50KiB/s\")."
333 (let ((start 0)
334 (progress nil))
335 (cond
336 ;; [download] 34.6% of ~2.61GiB at 929.50KiB/s ETA 01:01:57
337 ;; +---+ +------+ +---------+ +------+
338 ((or (string-equal youtube-dl-program "youtube-dl")
339 (string-equal (file-name-nondirectory youtube-dl-program) "youtube-dl"))
340 (while (string-match "\\[download\\] +\\([^ ]+%\\) +of *~? *\\([^ ]+\\) +at +\\([^ ]+\\) +ETA +\\([^ \n]+\\)" output start)
341 (let ((percent (match-string 1 output))
342 (total-size (match-string 2 output))
343 (download-rate (match-string 3 output))
344 (eta (match-string 4 output)))
345 (setf progress (list percent total-size download-rate eta)
346 start (match-end 0)))))
347 ((or (string-equal youtube-dl-program "yt-dlp")
348 (string-equal (file-name-nondirectory youtube-dl-program) "yt-dlp"))
349 ;; The output has non-ASCII shell color codes. Disable it with command-line option "--no-colors".
350 ;; [download] 0.4% of ~ 314.23MiB at 7.54KiB/s ETA 11:48:26 (frag 0/216)
351 ;; +--+ +-------+ +-------+ +------+
352 (while (string-match "\\[download\\] +\\([^ ]+%\\) +of *~? *\\([^ ]+\\) +at +\\([^ ]+\\) +ETA +\\([^ \n]+\\)" output start)
353 (let ((percent (match-string 1 output))
354 (total-size (match-string 2 output))
355 (download-rate (match-string 3 output))
356 (eta (match-string 4 output)))
357 ;; filter out not correct matched data.
358 ;; DEBUG:
359 ;; (message "[DEBUG] [youtube-dl] download-rate regexp matched: >>> %s <<<" download-rate)
360 ;; (message "[DEBUG] [youtube-dl] eta regexp matched: >>> %s <<<" eta)
361 (unless (or (string-equal-ignore-case total-size "Unknown.*")
362 (string-equal-ignore-case eta "Unknown.*") ; [download] 0.0% of 2.14MiB at Unknown B/s ETA Unknown
364 (setf progress (list percent total-size download-rate eta)
365 start (match-end 0)))))))
366 ;; DEBUG:
367 ;; (when progress
368 ;; (cl-destructuring-bind (percentage total-size download-rate eta) progress
369 ;; (message "[DEBUG] [youtube-dl] progress: percent: %s, total-size: %s, speed: %s, eta: %s."
370 ;; percentage total-size download-rate eta)))
371 progress))
373 (defun youtube-dl--get-destination (url)
374 "Return the destination filename for the given `URL' (if any).
375 The destination filename may potentially straddle two output
376 chunks, but this is incredibly unlikely. It's only used for
377 display purposes anyway.
379 $ youtube-dl --get-filename <URL>"
380 (message "[youtube-dl] getting video destination.")
381 (let* ((get-destination-buffer " *youtube-dl-get-destination*")
382 (get-destination-error-buffer " *youtube-dl-get-destination error*")
383 (destination))
384 ;; clear destination buffer content to clear destination history.
385 (progn
386 (when (buffer-live-p (get-buffer get-destination-buffer))
387 (with-current-buffer get-destination-buffer (erase-buffer)))
388 (when (buffer-live-p (get-buffer get-destination-error-buffer))
389 (with-current-buffer get-destination-error-buffer (erase-buffer))))
390 ;; retrieve destination
391 (make-process
392 :command (list youtube-dl-program "--get-filename" url)
393 :sentinel (lambda (proc event)
394 (if (string= event "finished\n")
395 (setq destination
396 (replace-regexp-in-string ; replace illegal filename characters in `destination'.
397 youtube-dl-destination-illegal-filename-chars-regex "-"
398 (replace-regexp-in-string
399 "\\ +\\[.*\\]" "" ; delete trailing [...] in filename.
400 (replace-regexp-in-string
401 "\n" "" ; delete trailing "\n" character.
402 (with-current-buffer get-destination-buffer (buffer-string))))))
403 (let* ((buffer-str (with-current-buffer get-destination-error-buffer (buffer-string)))
404 (match-regex "\\(ERROR\\|Error\\): \\(.*\\)")
405 (error-p (string-match-p match-regex buffer-str))
406 (matched-str (when (string-match match-regex buffer-str) (match-string 2 buffer-str))))
407 (if error-p
408 (progn
409 (message "[youtube-dl] `youtube-dl--get-destination' error!\n%s" matched-str)
410 ;; TODO:
411 ;; (funcall 'youtube-dl--get-destination url)
413 (progn
414 (message "[youtube-dl] get output filename success.")
415 ;; (kill-process proc)
416 ;; (kill-buffer (process-buffer proc))
417 )))))
418 :name "youtube-dl-get-destination"
419 :buffer get-destination-buffer
420 :stderr get-destination-error-buffer)
422 (setq destination-not-returned t)
423 (dotimes (i 10)
424 (when destination-not-returned
425 (sleep-for 1)
426 (if destination (setq destination-not-returned nil))))
428 ;; Prompt for user the destination interactively.
429 (unless destination
430 (setq destination (substring-no-properties (read-string "[youtube-dl] input destination: "))))
432 (when destination
433 (kill-new destination) ; copy to Emacs kill-ring for later operation.
434 (message "[youtube-dl] have got destination : %s" destination)
435 destination)))
437 ;;; TEST:
438 ;; (youtube-dl--get-destination "https://www.pornhub.com/view_video.php?viewkey=ph603b575e00170")
440 (defun youtube-dl--filter (proc output)
441 "The filter function of process PROC applied on OUTPUT."
442 (if (plist-get (process-plist proc) :item)
443 (let* ((item (plist-get (process-plist proc) :item))
444 (progress (youtube-dl--progress output))
445 (destination (youtube-dl-item-destination item)))
446 ;; Append to program log.
447 (let ((logged (list output)))
448 (if (and (youtube-dl-item-log item) (youtube-dl-item-log-end item)) ; make sure not nil.
449 (setf (cdr (youtube-dl-item-log-end item)) logged
450 (youtube-dl-item-log-end item) logged)
451 (setf (youtube-dl-item-log item) logged
452 (youtube-dl-item-log-end item) logged)))
453 ;; Update progress information.
454 (when progress
455 (cl-destructuring-bind (percentage total-size download-rate eta) progress
456 (setf (youtube-dl-item-percentage item) percentage
457 (youtube-dl-item-total-size item) total-size
458 (youtube-dl-item-download-rate item) download-rate
459 (youtube-dl-item-eta item) eta)))
460 ;; monitor process output whether error occurred?
461 (let* ((item-log-end (youtube-dl-item-log-end item))
462 (log-end-str (cond
463 ((stringp item-log-end) item-log-end)
464 ((listp item-log-end) (car (last item-log-end)))))
465 (error-hander (lambda () (setf (youtube-dl-item-running item) nil)
466 (message "[youtube-dl] ✖ <proc: %s>, destination: %s"
467 (youtube-dl-item-process item) (youtube-dl-item-destination item))))
468 (running-handler (lambda () (setf (youtube-dl-item-running item) t)))
469 (finished-handler (lambda () (setf (youtube-dl-item-running item) nil)
470 (message "[youtube-dl] ✔ %s downloaded." (youtube-dl-item-destination item)))))
471 (when (stringp log-end-str)
472 (cond
473 ;; progress finished when percentage is 100%.
474 ((or (string-equal (youtube-dl-item-percentage item) "100%")
475 (string-match "\\[download\\]\\ *100%.*" log-end-str)) ; ".*100%.*"
476 (funcall finished-handler))
477 ;; process exited abnormally with code 1
478 ((string-match "exited abnormally with code 1" log-end-str)
479 (funcall error-hander))
480 ;; file already downloaded
481 ((string-match "ERROR: Fixed output name but more than one file to download:.*" log-end-str)
482 ;; (funcall error-hander)
483 (message "[youtube-dl] ERROR: Already has existing downloading process or output file! (%s)" (youtube-dl-item-destination item)))
484 ;; any other errors
485 ((string-match "ERROR:.*" log-end-str)
486 (funcall error-hander))
487 (t (funcall running-handler)))))
488 ;; DEBUG:
489 ;; (message "[DEBUG] [youtube-dl] destination/title: %s" destination)
490 ;; Set item title to destination if it's empty.
491 (unless (youtube-dl-item-title item)
492 (setf (youtube-dl-item-title item) destination))
493 ;; write output to process associated buffer.
494 (with-current-buffer (process-buffer proc)
495 (let ((moving (= (point) (process-mark proc)))
496 ;; avoid process buffer read-only issue for `insert'.
497 (inhibit-read-only t))
498 (save-excursion
499 ;; Insert the text, advancing the process marker.
500 (goto-char (process-mark proc))
501 (insert output)
502 (set-marker (process-mark proc) (point)))
503 (if moving (goto-char (process-mark proc)))))
504 (youtube-dl--redisplay))))
506 (defun youtube-dl--construct-command (item)
507 "Construct full command of `youtube-dl' with arguments from ITEM."
508 (let ((url (youtube-dl-item-url item))
509 (slow-p (youtube-dl-item-slow-p item))
510 (rate-limit (youtube-dl-item-rate-limit item))
511 (destination (youtube-dl-item-destination item)))
512 (append
513 (list youtube-dl-program "--newline")
514 youtube-dl-extra-arguments
515 (when youtube-dl-omit-mtime (list "--no-mtime"))
516 (when youtube-dl-restrict-filenames (list "--restrict-filenames"))
517 ;; Disable non-ASCII shell color codes in output.
518 (cond
519 ((string-equal youtube-dl-program "youtube-dl") '("--no-color"))
520 ((string-equal youtube-dl-program "yt-dlp") '("--no-colors")))
521 (youtube-dl--proxy-append url)
522 (when slow-p `("--rate-limit" ,rate-limit))
523 (when destination `("--output" ,destination))
524 `("--" ,url))))
526 (defun youtube-dl--run-single-process (&optional item)
527 "Start `youtube-dl' ITEM downloading in single process."
528 (let ((item (youtube-dl--next))
529 (current-item (youtube-dl--current)))
530 (if (eq item current-item)
531 (youtube-dl--redisplay) ; do nothing, just display the youtube-dl-list buffer.
532 (if youtube-dl-process
533 (progn
534 ;; Switch to higher priority job, but offset error count first.
535 (cl-decf (youtube-dl-item-failures current-item))
536 (kill-process youtube-dl-process)) ; sentinel will clean up
537 ;; No subprocess running, start a one.
538 (let* ((url (substring-no-properties (youtube-dl-item-url item)))
539 (directory (youtube-dl-item-directory item))
540 (destination (youtube-dl-item-destination item))
541 (vid (youtube-dl-item-vid item))
542 (proc-buffer-name (youtube-dl--process-buffer-name vid))
543 (default-directory
544 (if directory
545 (concat (directory-file-name directory) "/")
546 (concat (directory-file-name youtube-dl-download-directory) "/")))
547 (_ (mkdir default-directory t))
548 (slow-p (youtube-dl-item-slow-p item))
549 (rate-limit (or (youtube-dl-item-rate-limit item) youtube-dl-slow-rate-limit))
550 (proc (make-process
551 :name proc-buffer-name
552 :command (let ((command (youtube-dl--construct-command item)))
553 ;; Insert complete command into process buffer for debugging.
554 (with-current-buffer (get-buffer-create proc-buffer-name)
555 (insert (format "Command: %s" command)))
556 command)
557 :sentinel #'youtube-dl--sentinel
558 :filter #'youtube-dl--filter
559 :buffer proc-buffer-name)))
560 (set-process-plist proc (list :item item))
561 (setf youtube-dl-process proc)
562 ;; mark item structure property `:running' to `t'.
563 (setf (youtube-dl-item-running item) t))))
564 (youtube-dl--redisplay)))
566 (defun youtube-dl--run-multiple-processes (&optional item)
567 "Start `youtube-dl' downloading ITEM in multiple processes."
568 (let* ((item (or item
569 youtube-dl--timer-item ; use item from `run-with-timer'.
570 (with-current-buffer (youtube-dl--list-buffer)
571 (nth (1- (line-number-at-pos)) youtube-dl-items))))
572 (current-item (youtube-dl--current)) ; `youtube-dl-process' currently actived process.
573 (proc (youtube-dl-item-process item)))
574 (unless (and item (youtube-dl-item-running item)) ; whether item structure property `:running' is `t'?
575 (let* ((url (substring-no-properties (youtube-dl-item-url item)))
576 (directory (youtube-dl-item-directory item))
577 (destination (youtube-dl-item-destination item))
578 (vid (youtube-dl-item-vid item))
579 (proc-buffer-name (youtube-dl--process-buffer-name vid))
580 (default-directory
581 (if directory
582 (concat (directory-file-name directory) "/")
583 (concat (directory-file-name youtube-dl-download-directory) "/")))
584 (_ (mkdir default-directory t))
585 (slow-p (youtube-dl-item-slow-p item))
586 (rate-limit (or (youtube-dl-item-rate-limit item) youtube-dl-slow-rate-limit))
587 (failures (youtube-dl-item-failures item))
588 (proc (when (<= failures 10) ; re-run process only when failures <= 10.
589 (make-process
590 :name proc-buffer-name
591 :command (let ((command (youtube-dl--construct-command item)))
592 ;; Insert complete command into process buffer for debugging.
593 (let ((inhibit-read-only t))
594 (with-current-buffer (get-buffer-create proc-buffer-name)
595 (insert (format "Command: %s" command))))
596 command)
597 :sentinel #'youtube-dl--sentinel
598 :filter #'youtube-dl--filter
599 :buffer proc-buffer-name))))
600 ;; clear temporary item variable `youtube-dl--timer-item'.
601 (setq youtube-dl--timer-item nil)
602 (when (processp proc)
603 ;; set process property list.
604 (set-process-plist proc (list :item item))
605 ;; assign `proc' object to item slot `:process'.
606 (setf (youtube-dl-item-process item) proc)
607 ;; set current youtube-dl process variable.
608 (setf youtube-dl-process proc)
609 ;; mark item structure property `:running' to `t'.
610 (setf (youtube-dl-item-running item) t))))
611 (youtube-dl--redisplay)))
613 (defun youtube-dl--run (&optional item)
614 "Start `youtube-dl' ITEM downloading."
615 ;; if single process model, then start download next item.
616 ;; if multiple processes model, then don't start next item `youtube-dl--run'.
617 (cl-case youtube-dl-process-model
618 (single-process (youtube-dl--run-single-process item))
619 (multiple-processes (youtube-dl--run-multiple-processes item))))
621 (defun youtube-dl--get-vid (url)
622 "Get video `URL' video vid with `youtube-dl' option `--get-id'."
623 (message "[youtube-dl] getting video vid.")
624 (let* ((parsed-url (url-generic-parse-url url))
625 (domain (url-domain parsed-url))
626 (parameters (url-filename parsed-url)))
627 ;; URL domain matching with regexp to extract vid.
628 (pcase domain
629 ("bilibili.com" ; "/video/BV1B8411L7te/", "/video/BV1uj411N7cp?spm_id_from=..0.0"
630 (when (string-match "/video/\\([^/?&]*\\)" parameters)
631 (match-string 1 parameters)))
632 ("pornhub.com" ; "/view_video.php?viewkey=ph6238f9c7cc9e2"
633 (when (string-match "/view_video\\.php\\?viewkey=\\([^/?&]*\\)" parameters)
634 (match-string 1 parameters)))
635 ("youtube.com" ; "/watch?v=q-iLEteyeUQ", "/watch?v=48JlgiBpw_I&t=311s"
636 (or (when (string-match "/watch\\?v=\\([^/?&]*\\)" parameters)
637 (match-string 1 parameters))
638 (when (string-match "\\(?:\\.be/\\|v=\\|v%3D\\|/shorts/\\|^\\)\\([-_a-zA-Z0-9]\\{11\\}\\)" url)
639 (match-string 1 url))))
640 ("hobby.porn" ; "https://hobby.porn/video/<vid>/"
641 (when (string-match "/video/\\([^/?&]*\\)" parameters)
642 (let ((vid (match-string 1 parameters)))
643 (if (length> vid 15)
644 (truncate-string-to-width (md5 (match-string 1 parameters)) 15)
645 vid))))
646 (_ (progn
647 (let* ((output (with-temp-buffer
648 (apply #'call-process
649 youtube-dl-program
650 nil t nil
651 (youtube-dl--proxy-append url "--get-id" url))
652 (buffer-string)))
653 (output-lines-list (seq-remove 'string-blank-p (string-lines output)))
654 (error-p (string-match-p "ERROR" output))
655 (warning-p (string-match-p "WARNING" (car output-lines-list))))
656 (cond
657 ;; "ERROR: Unsupported URL: ..."
658 ((string-match-p "ERROR: Unsupported URL:" output)
659 (user-error "[youtube-dl] Unsupported URL: %s" url))
660 ;; ERROR: [SpankBang] 7z268: Unable to download webpage: HTTP Error 403: Forbidden (caused by <HTTPError 403: Forbidden>)
661 ((string-match-p "Unable to download webpage: HTTP Error 403: Forbidden" output)
662 (user-error "[youtube-dl] Unable to download webpage: HTTP Error 403: Forbidden"))
663 ;; "WARNING: [generic] Falling back on generic information extractor"
664 ((or (string-equal-ignore-case (car output-lines-list) "WARNING: Falling back on generic information extractor")
665 (string-equal-ignore-case (car output-lines-list) "WARNING: [generic] Falling back on generic information extractor")
666 warning-p)
667 (truncate-string-to-width (car (last output-lines-list 1)) 15))
668 (error-p
669 (error (format "[youtube-dl] `youtube-dl--get-vid' retrive video vid error!\n%s" output)))
670 ;; TEST: (youtube-dl--get-vid "https://www.youtube.com/watch?v=VROjLiq9LeQ")
671 ;; ("VrAfJvZGGXE")
672 ((length= output-lines-list 1) (truncate-string-to-width (car output-lines-list) 15))
673 ;; TEST: (youtube-dl--get-vid "https://hobby.porn/video/<vid>")
674 ;; ("WARNING: [generic] Falling back on generic information extractor" "<vid>")
675 ((length= output-lines-list 2) (truncate-string-to-width (cadr output-lines-list) 15))
676 ;; TEST: (youtube-dl--get-vid "https://hanime1.me/watch?v=22454")
677 ;; ("WARNING: Falling back on generic information extractor." "watch?v=22454" "")
678 ((length= output-lines-list 3) (truncate-string-to-width (car (last output-lines-list 1)) 15))
679 ;; get vid failed, use URL string regex matching instead.
681 ;; truncate `vid' length to avoid breaking `youtube-dl-list' buffer the `vid' column width.
682 ;; TEST: (youtube-dl--get-vid "https://www.youtube.com/watch?v=1234567890abcdefghijklmnopqrstuvwxyz")
683 (truncate-string-to-width parameters 15)))))))))
685 ;;; TEST:
686 ;; (let ((parameters "/video/BV1B8411L7te/"))
687 ;; (when (string-match "/video/\\([^/]*\\)" parameters)
688 ;; (match-string 1 parameters)))
690 ;; (youtube-dl--get-vid "https://www.youtube.com/watch?v=<vid>")
691 ;; (youtube-dl--get-vid "https://www.pornhub.com/view_video.php?viewkey=<vid>")
692 ;; (youtube-dl--get-vid "https://hobby.porn/video/<vid>/")
694 ;;;###autoload
695 (cl-defun youtube-dl
696 (url &key title (priority 0) directory destination paused slow)
697 "Queues URL for download using `youtube-dl', returning the new item.
698 Download to DIRECTORY default in ~/Downloads/ with TITLE as filename.
699 Download in PRIORITY in downloading queue."
700 (interactive
701 (list (substring-no-properties
702 (read-from-minibuffer
703 "URL: " (or (thing-at-point 'url)
704 (when (eq major-mode 'org-mode)
705 (org-element-property :raw-link (org-element-context)))
706 (when interprogram-paste-function
707 (funcall interprogram-paste-function)))))))
708 ;; remove this ID failure only on youtube.com, use URL as ID. or use youtube-dl extracted title, or hash on URL.
709 (let* ((vid (youtube-dl--get-vid url))
710 (destination (or destination (youtube-dl--get-destination url)))
711 (title (or title
712 (replace-regexp-in-string (format "-%s.*" vid) "" destination)
713 (car (split-string destination "-"))))
714 (type (youtube-dl--get-type-for-extension (file-name-extension destination)))
715 (proc-buffer-name (youtube-dl--process-buffer-name vid))
716 (full-dir (expand-file-name (or directory youtube-dl-download-directory)))
717 (item (youtube-dl-item--create :url url
718 :vid vid
719 :failures 0
720 :priority priority
721 :paused-p paused
722 :slow-p slow
723 :rate-limit nil
724 :directory full-dir
725 :destination destination
726 :title title
727 :type type
728 :buffer proc-buffer-name
729 :process nil)))
730 (prog1 item
731 (unless (youtube-dl-item-running item)
732 (youtube-dl--add item) ; Add ITEM to the queue.
733 (youtube-dl--run item) ; Start ITEM in the queue.
734 (when youtube-dl-auto-show-list
735 (youtube-dl-list))))))
737 (defalias 'youtube-dl-download-video 'youtube-dl)
739 (defun youtube-dl--playlist-list (playlist)
740 "For each video in PLAYLIST, return one plist with :index, :vid, and :title."
741 (with-temp-buffer
742 (when (zerop (call-process youtube-dl-program nil t nil
743 "--ignore-config"
744 "--dump-json"
745 "--flat-playlist"
746 playlist))
747 (goto-char (point-min))
748 (cl-loop with json-object-type = 'plist
749 for index upfrom 1
750 for video = (ignore-errors (json-read))
751 while video
752 collect (list :index index
753 :vid (plist-get video :vid)
754 :title (plist-get video :title))))))
756 (defun youtube-dl--playlist-reverse (list)
757 "Return a copy of LIST with the indexes reversed."
758 (let ((max (cl-loop for entry in list
759 maximize (plist-get entry :index))))
760 (cl-loop for entry in list
761 for index = (plist-get entry :index)
762 for copy = (copy-sequence entry)
763 collect (plist-put copy :index (- (1+ max) index)))))
765 (defun youtube-dl--playlist-cutoff (list n)
766 "Return a sorted copy of LIST with all items except where :index < N."
767 (let ((key (lambda (v) (plist-get v :index)))
768 (filter (lambda (v) (< (plist-get v :index) n)))
769 (copy (copy-sequence list)))
770 (cl-delete-if filter (cl-stable-sort copy #'< :key key))))
772 ;;;###autoload
773 (cl-defun youtube-dl-download-playlist
774 (url &key directory (first 1) paused (priority 0) reverse slow)
775 "Add entire playlist URL to download queue, with index prefixes.
777 :directory PATH -- Destination DIRECTORY for all videos.
778 :first INDEX -- Start downloading from a given one-based FIRST index.
779 :paused BOOL -- Start all download entries as PAUSED.
780 :priority PRIORITY -- Use this PRIORITY for all download entries.
781 :reverse BOOL -- REVERSE video numbering, solve problem of reversed playlists.
782 :slow BOOL -- Start all download entries in SLOW mode."
783 (interactive
784 (list (read-from-minibuffer
785 "URL: "
786 (when interprogram-paste-function
787 (funcall interprogram-paste-function)))))
788 (message "[youtube-dl] fetching playlist ...")
789 (let ((videos (youtube-dl--playlist-list url)))
790 (if (null videos)
791 (error "Failed to fetch playlist (%s)" url)
792 (let* ((max (cl-loop for entry in videos
793 maximize (plist-get entry :index)))
794 (width (1+ (floor (log max 10))))
795 (prefix-format (format "%%0%dd" width)))
796 (when reverse
797 (setf videos (youtube-dl--playlist-reverse videos)))
798 (dolist (video (youtube-dl--playlist-cutoff videos first))
799 (let* ((index (plist-get video :index))
800 (prefix (format prefix-format index))
801 (title (format "%s-%s" prefix (plist-get video :title)))
802 (dest (format "%s-%s" prefix "%(title)s-%(id)s.%(ext)s")))
803 (youtube-dl (plist-get video :url)
804 :title title
805 :priority priority
806 :directory directory
807 :destination dest
808 :paused paused
809 :slow slow)))))))
811 ;;; The list user interface:
813 (defvar youtube-dl--list-buffer-name " *youtube-dl list*"
814 "The buffer name of `youtube-dl-list-mode'.")
816 (defun youtube-dl--list-buffer ()
817 "Returns the queue listing buffer."
818 (if-let ((buf (get-buffer-create youtube-dl--list-buffer-name)))
819 (with-current-buffer buf
820 ;; TODO: use `tabulated-list-mode'.
821 ;; (tabulated-list-mode)
822 (youtube-dl-list-mode)
823 (current-buffer))))
825 (defun youtube-dl--log-buffer (&optional item)
826 "Returns a `youtube-dl' log buffer for ITEM."
827 (when item
828 (let* ((name (youtube-dl-item-buffer item))
829 (buffer (get-buffer-create name)))
830 (with-current-buffer buffer
831 (unless (eq major-mode 'special-mode)
832 (special-mode))
833 (setf youtube-dl--log-item item)
834 (setf (youtube-dl-item-log item) item)
835 (current-buffer)))))
837 ;; refresh youtube-dl-list buffer (2).
838 (defun youtube-dl-list-redisplay ()
839 "Immediately redraw the queue list buffer."
840 (interactive)
841 (with-current-buffer (youtube-dl--list-buffer)
842 (let ((save-point (point))
843 (window (get-buffer-window (current-buffer))))
844 (youtube-dl--fill-listing)
845 (goto-char save-point)
846 (when window
847 (set-window-point window save-point))
848 (when hl-line-mode
849 (hl-line-highlight)))))
851 ;; refresh youtube-dl-list buffer (1).
852 (defun youtube-dl--redisplay ()
853 "Redraw the queue list buffer only if visible."
854 (let ((log-buffer (youtube-dl--log-buffer)))
855 (when log-buffer
856 (with-current-buffer log-buffer
857 (let ((inhibit-read-only t)
858 (saved-point (point))
859 (saved-point-max (point-max))
860 (window (get-buffer-window log-buffer)))
861 (erase-buffer)
862 (mapc #'insert (youtube-dl-item-log youtube-dl--log-item))
863 (when window
864 (set-window-point window (if (< saved-point saved-point-max)
865 saved-point
866 (point-max))))))))
867 (when (get-buffer-window (youtube-dl--list-buffer))
868 (youtube-dl-list-redisplay)))
870 (defun youtube-dl--pointed-item ()
871 "Get item under point. But signal an error if no item under point."
872 (unless (eq (current-buffer) (youtube-dl--list-buffer))
873 (user-error "The operation is ONLY available in `%s' buffer" youtube-dl--list-buffer-name))
874 (let ((item (nth (1- (line-number-at-pos)) youtube-dl-items)))
875 (if item item (error "[youtube-dl] No item at point"))))
877 (defun youtube-dl-list-log ()
878 "Display the log of the video under point."
879 (interactive)
880 (let* ((item (youtube-dl--pointed-item))
881 (buffer (youtube-dl--log-buffer item)))
882 (if item
883 (progn
884 (display-buffer buffer)
885 (select-window (get-buffer-window buffer))
886 (youtube-dl--redisplay))
887 (error "[youtube-dl] Current item under point return nil"))))
889 (defun youtube-dl-list-kill-log ()
890 "Kill the `youtube-dl' log buffer."
891 (interactive)
892 (let ((buffer (youtube-dl--log-buffer)))
893 (when buffer
894 (kill-buffer buffer))))
896 (defun youtube-dl-list-yank ()
897 "Copy the URL of the video under point to the clipboard."
898 (interactive)
899 (when-let* ((item (youtube-dl--pointed-item))
900 (url (youtube-dl-item-url item)))
901 (kill-new url)
902 (message "[youtube-dl] yanked %s" url)))
904 (defun youtube-dl-list-kill ()
905 "Remove the selected item from the queue without deleting downloaded files."
906 (interactive)
907 (when-let* ((_ youtube-dl-items) ; avoid `youtube-dl-items' is `nil'.
908 (item (youtube-dl--pointed-item))
909 (proc (youtube-dl-item-process item)))
910 (youtube-dl--remove item)
911 (youtube-dl-list-redisplay)
912 (message "[youtube-dl] process %s is killed." proc)))
914 (defun youtube-dl-list-pause (&optional item)
915 "Pause downloading of ITEM under point."
916 (interactive)
917 (let* ((item (or item (youtube-dl--pointed-item)))
918 (proc (youtube-dl-item-process item))
919 (paused-p (youtube-dl-item-paused-p item)))
920 (unless (and item paused-p)
921 ;; kill the process, but keep item on list buffer for pause status.
922 (when (process-live-p proc) (kill-process proc))
923 ;; after killed process, also setting item structure status properties.
924 (setf (youtube-dl-item-paused-p item) t)
925 (setf (youtube-dl-item-running item) nil))
926 (youtube-dl-list-redisplay)
927 (message "[youtube-dl] process %s is paused." proc)))
929 (defun youtube-dl-list-resume (&optional item)
930 "Resume failure/paused downloading ITEM under point."
931 (interactive)
932 (when-let* ((item (or item (youtube-dl--pointed-item)))
933 (proc (youtube-dl-item-process item)))
934 (when item
935 (setf (youtube-dl-item-failures item) 0)
936 (cond
937 ((youtube-dl-item-paused-p item)
938 (setf (youtube-dl-item-paused-p item) nil)
939 (youtube-dl--run))
940 ((not (youtube-dl-item-running item))
941 (youtube-dl--run))
943 (setf (youtube-dl-item-running item) nil)
944 (youtube-dl--run)
945 ;; (user-error (format "[youtube-dl] Can't resume process %s correctly. Try resume again." proc))
947 (youtube-dl-list-redisplay)
948 (message "[youtube-dl] process %s is resumed." proc))))
950 (defun youtube-dl-list-toggle-pause (&optional item)
951 "Toggle pause downloading of ITEM under point."
952 (interactive)
953 (let* ((item (or item (youtube-dl--pointed-item)))
954 (paused-p (youtube-dl-item-paused-p item)))
955 (if (and item paused-p)
956 (youtube-dl-list-resume item)
957 (youtube-dl-list-pause item))))
959 (defun youtube-dl-list-toggle-pause-all ()
960 "Toggle pause downloading of all items."
961 (interactive)
962 (let* ((item (youtube-dl--pointed-item))
963 (paused-p (youtube-dl-item-paused-p item))))
964 (if paused-p
965 (dolist (item youtube-dl-items)
966 (youtube-dl-list-pause item))
967 (dolist (item youtube-dl-items)
968 (youtube-dl-list-resume item))))
970 (defun youtube-dl-list-toggle-slow (item &optional rate-limit)
971 "Set slow RATE-LIMIT on ITEM under point."
972 (interactive (youtube-dl--pointed-item))
973 (when item
974 (let ((proc (youtube-dl-item-process item))
975 (slow-p (youtube-dl-item-slow-p item))
976 (rate-limit (substring-no-properties
977 (or rate-limit
978 (completing-read "[youtube-dl] limit download rate (e.g. 100K): "
979 '("20K" "50K" "100K" "200K" "500K" "1M" "2M"))))))
980 ;; restart with a slower download rate.
981 (when (process-live-p proc) (kill-process proc))
982 (setf (youtube-dl-item-slow-p item) (not slow-p))
983 (setf (youtube-dl-item-rate-limit item) rate-limit)
984 (youtube-dl--run)))
985 (youtube-dl-list-redisplay))
987 (defun youtube-dl-list-toggle-slow-all ()
988 "Set slow rate on all items."
989 (interactive)
990 (let* ((count (length youtube-dl-items))
991 (slow-count (cl-count-if #'youtube-dl-item-slow-p youtube-dl-items))
992 (target (< slow-count (- count slow-count)))
993 (rate-limit (completing-read "[youtube-dl] limit download rate (e.g. 100K): "
994 '("20K" "50K" "100K" "200K" "500K" "1M" "2M"))))
995 (dolist (item youtube-dl-items)
996 (youtube-dl-list-toggle-slow item rate-limit)))
997 (youtube-dl--redisplay))
999 (defun youtube-dl-list-priority-modify (delta)
1000 "Change priority of item under point by DELTA."
1001 (when-let ((item (youtube-dl--pointed-item)))
1002 (cl-incf (youtube-dl-item-priority item) delta)
1003 (youtube-dl--run)))
1005 (defun youtube-dl-list-priority-up ()
1006 "Decrease priority of item under point."
1007 (interactive)
1008 (youtube-dl-list-priority-modify 1))
1010 (defun youtube-dl-list-priority-down ()
1011 "Increase priority of item under point."
1012 (interactive)
1013 (youtube-dl-list-priority-modify -1))
1015 (defun youtube-dl-list-next-item ()
1016 "Move to next item in listing buffer."
1017 (interactive)
1018 (unless (zerop (forward-line 1)) (user-error "End of buffer"))
1019 (unless (eobp) (beginning-of-line)))
1021 (defun youtube-dl-list-prev-item ()
1022 "Move to previous item in listing buffer."
1023 (interactive)
1024 (when (<= (line-number-at-pos) 1)
1025 (user-error "Beginning of buffer"))
1026 (forward-line -1)
1027 (beginning-of-line))
1029 (defvar youtube-dl-list-mode-map
1030 (let ((map (make-sparse-keymap)))
1031 (prog1 map
1032 (define-key map "a" #'youtube-dl)
1033 (define-key map "g" #'youtube-dl-list-redisplay)
1034 (define-key map "l" #'youtube-dl-list-log)
1035 (define-key map "L" #'youtube-dl-list-kill-log)
1036 (define-key map "y" #'youtube-dl-list-yank)
1037 (define-key map "k" #'youtube-dl-list-kill)
1038 (define-key map "p" #'youtube-dl-list-toggle-pause)
1039 (define-key map "P" #'youtube-dl-list-toggle-pause-all)
1040 (define-key map "r" #'youtube-dl-list-resume)
1041 (define-key map "s" #'youtube-dl-list-toggle-slow)
1042 (define-key map "S" #'youtube-dl-list-toggle-slow-all)
1043 (define-key map "]" #'youtube-dl-list-priority-up)
1044 (define-key map "[" #'youtube-dl-list-priority-down)
1045 (define-key map [down] #'youtube-dl-list-next-item)
1046 (define-key map [up] #'youtube-dl-list-prev-item)
1047 (define-key map "q" #'youtube-dl-list-close-window)))
1048 "Keymap for `youtube-dl-list-mode'.")
1050 (defun youtube-dl-list-close-window ()
1051 "Close the \"*youtube-dl list*\" buffer window."
1052 (interactive)
1053 (delete-window (get-buffer-window youtube-dl--list-buffer-name)))
1055 (defvar-local youtube-dl-list--auto-close-window-timer nil
1056 "A timer to auto close \"*youtube-dl list*\" window.")
1058 (defun youtube-dl-list--auto-close-window ()
1059 "Auto close \"*youtube-dl list*\" buffer after finished all downloading."
1060 (when (equal (length youtube-dl-items) 0)
1061 (when-let ((buf (get-buffer-window (youtube-dl--list-buffer))))
1062 (delete-window buf)
1063 (when (timerp youtube-dl-list--auto-close-window-timer)
1064 (cancel-timer youtube-dl-list--auto-close-window-timer)))))
1066 (defvar youtube-dl-list--format
1067 ;; (percentage download-rate)
1068 ;; v
1069 ;;space,vid,progress size eta fails
1070 ;;| | | | | | status
1071 ;;| | | | | | | title
1072 ;;v v v v v v v v
1073 "%s%-16s %-22.22s %-10.10s %-6.6s %-7.7s %-8.8s %s"
1074 "Define the `youtube-dl-list-mode' `header-line-format' and list item format.")
1076 (define-derived-mode youtube-dl-list-mode special-mode "youtube-dl"
1077 "Major mode for listing the `youtube-dl' download queue."
1078 :group 'youtube-dl
1079 (use-local-map youtube-dl-list-mode-map)
1080 (hl-line-mode)
1081 (setq-local truncate-lines t) ; truncate long line text.
1082 (setf header-line-format
1083 (propertize
1084 (format youtube-dl-list--format
1085 (propertize " " 'display '((space :align-to 0))) ; space
1086 " vid " "| progress" "| size" "| ETA" "| fails" "| status"
1087 (concat "| title " (make-string 100 (string-to-char " "))))
1088 'face '(:inverse-video t :extend t))))
1090 (defface youtube-dl-type-video
1091 '((t :foreground "green"))
1092 "Face for video type."
1093 :group 'youtube-dl)
1095 (defface youtube-dl-type-audio
1096 '((t :foreground "DeepSkyBlue"))
1097 "Face for audio type."
1098 :group 'youtube-dl)
1100 (defface youtube-dl-type-subtitle
1101 '((t :foreground "dark gray"))
1102 "Face for subtitle type."
1103 :group 'youtube-dl)
1105 (defface youtube-dl-type-thumbnail
1106 '((t :foreground "pink"))
1107 "Face for thumbnail type."
1108 :group 'youtube-dl)
1110 (defun youtube-dl--face-for-type (type)
1111 "Return the face for TYPE."
1112 (cl-case type
1113 (video 'youtube-dl-type-video)
1114 (audio 'youtube-dl-type-audio)
1115 (subtitle 'youtube-dl-type-subtitle)
1116 (thumbnail 'youtube-dl-type-thumbnail)))
1118 (defun youtube-dl--get-type-for-extension (extension)
1119 "Return media type depend on EXTENSION."
1120 (cond
1121 ((member extension '("avi" "rmvb" "ogg" "ogv" "mp4" "mkv" "mov" "webm" "flv" "ts" "mpg"))
1122 'video)
1123 ((member extension '("flac" "mp3" "wav" "m4a"))
1124 'audio)
1125 ((member extension '("ass" "srt" "sub" "vtt" "ssf"))
1126 'subtitle)
1127 ((member extension '("heic" "svg" "webp" "png" "gif" "tiff" "jpeg" "jpg" "xpm" "xbm" "pbm"))
1128 'thumbnail)))
1130 ;;; refresh youtube-dl-list buffer (3).
1131 (defun youtube-dl--fill-listing ()
1132 "Erase and redraw the queue in the queue listing buffer."
1133 (with-current-buffer (youtube-dl--list-buffer)
1134 (let* ((inhibit-read-only t)
1135 (active (youtube-dl--current)))
1136 (erase-buffer)
1137 (dolist (item youtube-dl-items)
1138 (let ((vid (youtube-dl-item-vid item))
1139 (running (youtube-dl-item-running item))
1140 (failures (youtube-dl-item-failures item))
1141 (priority (youtube-dl-item-priority item))
1142 (percentage (youtube-dl-item-percentage item))
1143 (download-rate (youtube-dl-item-download-rate item))
1144 (paused-p (youtube-dl-item-paused-p item))
1145 (slow-p (youtube-dl-item-slow-p item))
1146 (rate-limit (youtube-dl-item-rate-limit item))
1147 (total-size (youtube-dl-item-total-size item))
1148 (eta (youtube-dl-item-eta item))
1149 (type (youtube-dl-item-type item))
1150 (title (youtube-dl-item-title item))
1151 (url (youtube-dl-item-url item)))
1152 (insert
1153 (propertize
1154 (format (concat youtube-dl-list--format "\n")
1155 ;; space
1157 ;; (propertize " " 'display '((space :align-to 0)))
1158 ;; vid
1159 (if running ; update `:running' property every time process update.
1160 (propertize vid 'face 'default)
1161 (propertize vid 'face 'youtube-dl-pause))
1162 ;; progress (percentage download-rate)
1163 (if (and percentage download-rate)
1164 (propertize (format "⦼%-5.5s ⇩%-17.17s " percentage download-rate) 'face 'youtube-dl-active)
1165 (propertize (format "⦼%-5.5s ⇩%-17.17s " percentage download-rate) 'face 'youtube-dl-pause))
1166 ;; size
1167 (or (propertize (format "%s" total-size) 'face 'youtube-dl-pause) "-")
1168 ;; eta
1169 (or (propertize (format "%s" eta) 'face 'youtube-dl-pause) "?")
1170 ;; failure
1171 (if (= failures 0)
1173 (propertize (format " [%d] " failures) 'face 'youtube-dl-failure))
1174 ;; priority
1175 ;; (if (= priority 0)
1176 ;; ""
1177 ;; (propertize (format "%+d " priority) 'face 'youtube-dl-priority))
1178 ;; status
1179 (concat
1180 (if slow-p (propertize "SLOW" 'face 'youtube-dl-slow))
1181 (if rate-limit (propertize (format "≤ %s" rate-limit) 'face 'youtube-dl-slow))
1182 (if paused-p (propertize "PAUSE" 'face 'youtube-dl-pause)))
1183 ;; title
1184 (or (propertize title 'face (youtube-dl--face-for-type type)) ""))
1185 'url url)))))))
1187 ;;;###autoload
1188 (defun youtube-dl-list ()
1189 "Display a list of all videos queued for download."
1190 (interactive)
1191 (youtube-dl--fill-listing)
1192 ;; set timer to auto close `youtube-dl--list-buffer' "*youtube-dl list*" buffer after finished all downloading.
1193 (when (and youtube-dl-auto-close-list-buffer
1194 (buffer-live-p (youtube-dl--list-buffer)))
1195 (with-current-buffer (youtube-dl--list-buffer)
1196 (unless (timerp youtube-dl-list--auto-close-window-timer)
1197 (setq-local youtube-dl-list--auto-close-window-timer
1198 (run-with-timer 0 20 #'youtube-dl-list--auto-close-window)))
1199 ;; jump to beginning of buffer.
1200 (goto-char (point-min))))
1201 (display-buffer (youtube-dl--list-buffer)))
1203 ;;;###autoload
1204 (defun youtube-dl-download-audio (url)
1205 "Download audio format of URL."
1206 (interactive
1207 (list (read-from-minibuffer
1208 "URL: " (or (thing-at-point 'url)
1209 (when interprogram-paste-function
1210 (funcall interprogram-paste-function))))))
1211 (let ((youtube-dl-extra-arguments
1212 (append youtube-dl-extra-arguments '("--extract-audio" "--audio-format" "mp3" "--embed-thumbnail"))))
1213 (youtube-dl url)))
1215 ;;;###autoload
1216 (defun youtube-dl-download-subtitle (url)
1217 "Download video subtitle of URL."
1218 (interactive
1219 (list (read-from-minibuffer
1220 "URL: " (or (thing-at-point 'url)
1221 (when interprogram-paste-function
1222 (funcall interprogram-paste-function))))))
1223 (let ((youtube-dl-extra-arguments
1224 (append youtube-dl-extra-arguments
1225 '("--skip-download"
1226 "--write-sub" ; write subtitle file
1227 "--write-auto-sub" ; write auto generated subtitle file
1228 "--sub-lang" "en,zh-Hans" ; use --list-subs for a list of available language tags.
1229 "--sub-format" "ass/srt/best" ; ass/srt/best, srt, vtt, ttml, srv3, srv2, srv1
1230 ))))
1231 (youtube-dl url)))
1233 ;;;###autoload
1234 (defun youtube-dl-download-thumbnail (url)
1235 "Download video thumbnail of URL."
1236 (interactive
1237 (list (read-from-minibuffer
1238 "URL: " (or (thing-at-point 'url)
1239 (when interprogram-paste-function
1240 (funcall interprogram-paste-function))))))
1241 (let ((youtube-dl-extra-arguments
1242 (append youtube-dl-extra-arguments '("--skip-download" "--write-thumbnail"))))
1243 (youtube-dl url)))
1245 ;;;###autoload
1246 (defun youtube-dl-download-to-directory (url)
1247 "Download video of URL to a interactively selected directory.
1249 Available options:
1250 - current working directory
1251 - `org-attach' directory"
1252 (interactive
1253 (list (read-from-minibuffer
1254 "URL: " (or (thing-at-point 'url)
1255 (when interprogram-paste-function
1256 (funcall interprogram-paste-function))
1257 (car (ignore-errors (org-entry-get-multivalued-property nil "URL")))))))
1258 (let* ((video-or-audio? (yes-or-no-p "[youtube-dl] download video [y] or audio [n] ? "))
1259 (youtube-dl-extra-arguments (if video-or-audio?
1260 (append youtube-dl-extra-arguments
1261 '("--embed-metadata" "--embed-thumbnail"))
1262 (append youtube-dl-extra-arguments
1263 '("--extract-audio" "--audio-format" "mp3" "--embed-thumbnail"))))
1264 (destination (read-string "[youtube-dl] Input filename: "
1265 (let ((destination (youtube-dl--get-destination url)))
1266 (if (eq major-mode 'org-mode)
1267 (concat (substring-no-properties (org-get-heading t t t t))
1269 (if video-or-audio?
1270 (or (file-name-extension destination) "mp4")
1271 "mp3"))
1272 destination))))
1273 (title (file-name-base destination))
1274 (directory (let* ((dir-org-attach (when (eq major-mode 'org-mode)
1275 (if (org-attach-dir) (org-attach-dir)
1276 (org-attach-dir-get-create))))
1277 (dir-list (list dir-org-attach
1278 default-directory
1279 youtube-dl-download-directory)))
1280 (completing-read "[youtube-dl] Select download directory: "
1281 dir-list 'stringp nil nil nil dir-org-attach nil))))
1282 (youtube-dl url :destination destination :title title :directory directory)
1283 ;; use `org-insert-link'
1284 ;; (if (string-empty-p destination)
1285 ;; (org-insert-link t)
1286 ;; (org-insert-link nil (expand-file-name destination directory) destination))
1287 ;; or use `org-insert-last-stored-link'
1288 (cl-case org-attach-store-link-p
1289 (attached
1290 (push (list (concat "attachment:" destination) destination) org-stored-links))
1291 (file
1292 (push (list (concat "file:" (file-name-concat (org-attach-dir) destination)) destination) org-stored-links)))
1293 (message "[youtube-dl] You can press [C-c C-l] `org-insert-last-stored-link' to insert link.")
1294 ;; insert last stored link when on empty line
1295 (when (equal (line-beginning-position) (line-end-position))
1296 (call-interactively 'org-insert-last-stored-link))))
1299 (provide 'youtube-dl)
1301 ;;; youtube-dl.el ends here