summaryrefslogtreecommitdiff
path: root/modules/media-utils.el
blob: e4eccb5e7442a684a016a66c2563095b4e6eb922 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
;;; media-utils.el --- Utilities for Downloading and Viewing Media  -*- coding: utf-8; lexical-binding: t; -*-
;;
;;; Commentary:
;;
;; This library provides reusable Emacs methods for working with online and
;; local media, to support media download and playback from Emacs.
;;
;; Main features:
;;
;; - Asynchronously download videos (e.g., from YouTube and similar sites) via
;;   yt-dlp, with queueing and background management handled by the
;;   task-spooler (tsp) utility.
;;
;; - Asynchronously play media URLs using a user-defined choice of external
;;   media players (mpv, VLC, etc.), with automatic stream resolution via
;;   yt-dlp when required, and dynamic configuration of playback options.
;;
;; - Default media player selection via `cj/default-media-player', allowing
;;   users to set their preferred player (mpv, VLC, IINA, MPlayer, etc.) which
;;   will be used automatically when playing media.
;;
;; - Customizable media player configurations in `cj/media-players', with
;;   support for different yt-dlp format preferences, command-line arguments,
;;   and stream URL handling for each player.
;;
;;; Code:

;; Declare functions and variables from other modules
(declare-function cj/log-silently "system-utils" (format-string &rest args))
(defvar videos-dir) ;; from user-constants.el

;; ------------------------ Default Media Configurations -----------------------
;; Common yt-dlp format codes:
;; 18  - 360p MP4 (good for low bandwidth)
;; 22  - 720p MP4 (good balance of quality and size)
;; best - best single file format
;; best[height<=720] - best format up to 720p
;; best[height<=1080] - best format up to 1080p
;; bestvideo+bestaudio - best video and audio (may require ffmpeg)
;; For more formats, run: yt-dlp -F <youtube-url>

(defcustom cj/media-players
  '((mpv . (:command "mpv"
                     :args nil
					 :name "MPV"
					 :needs-stream-url nil
					 :yt-dlp-formats nil))
	(vlc . (:command "vlc"
					 :args nil
					 :name "VLC"
					 :needs-stream-url t
					 :yt-dlp-formats ("22" "18" "best")))  ; Try formats in order
	(cvlc . (:command "cvlc"
					  :args "-vvv"
					  :name "cvlc"
					  :needs-stream-url t
					  :yt-dlp-formats ("22" "18" "best")))
	(mplayer . (:command "mplayer"
						 :args nil
						 :name "MPlayer"
						 :needs-stream-url t
						 :yt-dlp-formats ("18" "22" "best")))
	(iina . (:command "iina"
					  :args nil
					  :name "IINA"
					  :needs-stream-url t
					  :yt-dlp-formats ("best[height<=1080]" "22" "best"))))
  "Define media players and their configurations for yt-dlp and playing.
Each entry is (SYMBOL . PLIST). The PLIST accepts the keys :command for the
executable name, :args for optional arguments, :name for a human-readable
label, :needs-stream-url for a boolean flag indicating whether to extract a
stream URL with `yt-dlp', and :yt-dlp-formats for a prioritized list of format
strings."
  :type '(alist :key-type symbol
				:value-type (plist :key-type keyword
								   :value-type sexp))
  :group 'media)

(defcustom cj/default-media-player 'vlc
  "The default media player to use for videos.
Should be a key from `cj/media-players'."
  :type 'symbol
  :group 'media)

(defun cj/get-available-media-players ()
  "Return a list of available media players from `cj/media-players'."
  (cl-loop for (player . config) in cj/media-players
		   when (executable-find (plist-get config :command))
		   collect player))

(defun cj/select-media-player ()
  "Interactively select a media player from available options."
  (interactive)
  (let* ((available (cj/get-available-media-players))
		 (choices (mapcar (lambda (player)
							(let ((config (alist-get player cj/media-players)))
							  (cons (plist-get config :name) player)))
						  available))
		 (selection (completing-read
					 (format "Select media player (current: %s): "
							 (plist-get (alist-get cj/default-media-player cj/media-players) :name))
					 choices nil t))
		 (player (alist-get selection choices nil nil #'string=)))
	(when player
	  (setq cj/default-media-player player)
	  (message "Media player set to: %s" selection))))

;; ---------------------- Playing Via Default Media Player ---------------------

(defun cj/media-play-it (url)
  "Play the URL with the configured media player in an async process."
  (let* ((player-config (alist-get cj/default-media-player cj/media-players))
		 (command (plist-get player-config :command))
		 (args (plist-get player-config :args))
		 (player-name (plist-get player-config :name))
		 (needs-stream-url (plist-get player-config :needs-stream-url))
		 (yt-dlp-formats (plist-get player-config :yt-dlp-formats))
		 (url-display (truncate-string-to-width url 50)))

	(unless (executable-find command)
	  (error "%s is not installed or not in PATH" player-name))

	(let* ((buffer-name (format "*%s: %s*" player-name url-display))
		   (shell-command
			(if needs-stream-url
				;; Use shell substitution with yt-dlp
				(let ((format-string (if yt-dlp-formats
										 (format "-f %s"
												 (mapconcat #'shell-quote-argument
															yt-dlp-formats
															"/"))
									   "")))
				  (format "%s %s $(%s %s -g %s)"
						  command
						  (or args "")
						  "yt-dlp"
						  format-string
						  (shell-quote-argument url)))
			  ;; Direct playback without yt-dlp
			  (format "%s %s %s"
					  command
					  (or args "")
					  (shell-quote-argument url)))))

	  (message "Playing with %s: %s" player-name url-display)
	  (cj/log-silently "DEBUG: Executing: %s" shell-command)

	  (let ((process (start-process-shell-command
					  player-name
					  buffer-name
					  shell-command)))
		(set-process-sentinel
		 process
		 (lambda (proc event)
		   (cond
			((string-match-p "finished" event)
			 (message "✓ Finished playing: %s" url-display))
			((string-match-p "exited abnormally" event)
			 (message "✗ Playback failed: %s" url-display)
			 (with-current-buffer (process-buffer proc)
			   (goto-char (point-min))
			   (when (re-search-forward "ERROR:" nil t)
				 (cj/log-silently "DEBUG: yt-dlp error: %s"
								  (buffer-substring-no-properties
								   (line-beginning-position)
								   (line-end-position)))))))
		   (when (string-match-p "finished\\|exited" event)
			 (kill-buffer (process-buffer proc)))))))))

;; ------------------------- Media-Download Via yt-dlp -------------------------

(defun cj/yt-dl-it (url)
  "Downloads the URL in an async shell."
  (unless (executable-find "yt-dlp")
	(error "The program yt-dlp is not installed or not in PATH"))
  (unless (executable-find "tsp")
	(error "The tsp (task-spooler) program is not installed or not in PATH"))
  (let* ((default-directory videos-dir)
		 (buffer-name (format "*yt-dlp: %s*" (truncate-string-to-width url 50)))
		 (output-template (format "%s/%%(channel)s-%%(title)s.%%(ext)s" videos-dir))
		 (url-display (truncate-string-to-width url 50))
		 (process (start-process "yt-dlp" buffer-name
								 "tsp" "yt-dlp" "--add-metadata" "-ic"
								 "-o" output-template url)))
	(message "Started download: %s" url-display)
	(set-process-sentinel process
						  (lambda (proc event)
							(cond
							 ((string-match-p "finished" event)
							  (message "✓ Finished downloading: %s" url-display))
							 ((string-match-p "exited abnormally" event)
							  (message "✗ Download failed: %s" url-display)))
							(when (string-match-p "finished\\|exited" event)
							  (kill-buffer (process-buffer proc)))))))

(provide 'media-utils)
;;; media-utils.el ends here.