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