feat: torrent to mpv - nyaa search, transmission sequential, mpv overlay

This commit is contained in:
2026-08-11 11:08:46 -04:00
parent b344d35ddd
commit d79fae86d0
3 changed files with 602 additions and 0 deletions

View File

@@ -4284,3 +4284,306 @@ Need chrome... :(
:localleader
:desc "Edit Edna rules" "E" #'org-edna-edit))
#+end_src
* +Torrent to MPV+
Search Nyaa.si from Emacs → add to Transmission with sequential download
→ watch with mpv before the download finishes.
#+begin_src emacs-lisp
;;; Torrent to MPV — Nyaa search + Transmission + mpv playback
;; dom.el provides dom-by-tag, dom-text for RSS XML parsing
(require 'dom)
;; ── Transmission daemon config ──
;; NB: no (require 'transmission) here — Doom autoloads it on first use.
(after! transmission
(setq transmission-host "localhost"
transmission-service 9091
transmission-refresh-interval 3
transmission-refresh-modes '(transmission-mode transmission-files-mode)))
;; ── Sequential download (add torrent + play) ──
(defun gortium/transmission-add-sequential (magnet &optional directory callback)
"Add MAGNET to Transmission with sequential download enabled.
If CALLBACK is provided, call it with the torrent name on success."
(interactive "sMagnet: ")
(transmission-request-async
(lambda (response)
(let-alist response
(let ((name (or .torrent-added.name .torrent-duplicate.name)))
(if name
(progn
(message "Added: %s (sequential)" name)
(when callback (funcall callback name)))
(message "Added torrent (sequential)")))))
"torrent-add"
(append `(:filename ,(if (transmission-btih-p magnet)
(concat "magnet:?xt=urn:btih:" magnet)
magnet)
:sequential_download t
:paused :json-false)
(when directory (list :download-dir (expand-file-name directory))))))
;; ── Play file from Transmission file list ──
(defun gortium/transmission-play-file (&optional file)
"Play FILE (or file at point) with mpv overlay.
Works on partial downloads (.part files) too."
(interactive)
(let ((path (or file
(condition-case nil
(transmission-files-file-at-point)
(error nil)))))
(if (null path)
(user-error "No file at point or file not found")
(let ((real-path
(or (and (file-exists-p path) path)
(and (file-exists-p (concat path ".part"))
(concat path ".part"))
;; Check incomplete dir too
(let ((inc (replace-regexp-in-string
"/complete/" "/incomplete/" path)))
(or (and (file-exists-p inc) inc)
(and (file-exists-p (concat inc ".part"))
(concat inc ".part")))))))
(unless real-path
(user-error "File not found: %s" path))
(start-process "transmission-mpv" nil "mpv"
"--quiet" "--really-quiet"
"--geometry=30%x30%-30-50" "--ontop" "--no-border"
"--title=Transmission MPV"
real-path)
(message "Playing: %s" (file-name-nondirectory real-path))))))
;; Override transmission-find-file to always play via mpv instead
;; (advice catches all callers: major mode, Evil, menu, M-x)
(defun gortium/--play-advice (&rest _)
"Redirect find-file to mpv play for partial downloads."
(call-interactively #'gortium/transmission-play-file))
(advice-add 'transmission-find-file :around #'gortium/--play-advice)
;; ══════════════════════════════════════════════════
;; Nyaa.si Search
;; ══════════════════════════════════════════════════
(defvar gortium/nyaa-base-url "https://nyaa.si")
(defvar gortium/nyaa-search-history '())
(defvar-local gortium/nyaa-results nil
"Alist of (ID . plist) for current Nyaa search buffer.")
(defvar gortium/nyaa-category-short
'(("1_1" . "AMV") ("1_2" . "Eng") ("1_3" . "NonEng")
("1_4" . "Raw") ("2_1" . "Flac") ("2_2" . "Lossy")
("3_1" . "L-Eng") ("3_2" . "L-NonE") ("3_3" . "L-Raw")
("4_1" . "LiveEng") ("4_2" . "LiveNEn")("4_3" . "LiveRaw")
("5_1" . "SW-App") ("5_2" . "SW-Game")("6_1" . "XXX"))
"Short category labels for column display.")
(defun gortium/nyaa-cat-label (id)
(or (cdr (assoc id gortium/nyaa-category-short)) id))
(defvar gortium/nyaa-search-query nil
"Last query string, for reverting.")
;; ── Interactive search ──
(defun gortium/nyaa-search (query &optional trusted-only)
"Search Nyaa.si RSS for QUERY, display results in tabulated-list.
With \\[universal-argument], show trusted uploads only."
(interactive
(list (read-string "Nyaa search: " nil 'gortium/nyaa-search-history)
current-prefix-arg))
(setq gortium/nyaa-search-query query)
(let ((url (format "%s/?page=rss&q=%s&s=seeders&o=desc%s"
gortium/nyaa-base-url
(url-encode-url query)
(if trusted-only "&f=1" ""))))
(message "Searching Nyaa for \"%s\"..." query)
(let ((buf (get-buffer-create "*Nyaa Search*")))
(with-current-buffer buf (setq gortium/nyaa-results nil)))
(url-retrieve url #'gortium/nyaa--handle-response (list query))))
;; ── RSS response handler ──
(defun gortium/nyaa--handle-response (status query)
"Handle Nyaa RSS response."
(condition-case err
(let ((results (gortium/nyaa--parse-rss)))
(if (null results)
(progn
(message "Nyaa: no results for \"%s\"" query)
(when (buffer-local-value 'gortium/nyaa-search-query
(get-buffer "*Nyaa Search*"))
(with-current-buffer (get-buffer-create "*Nyaa Search*")
(setq mode-name "Nyaa-Search (no results)"))))
(gortium/nyaa--display-results results query)))
(error (message "Nyaa search error: %S" err))))
(defun gortium/nyaa--parse-rss ()
"Parse Nyaa RSS XML from current buffer into plist list.
Returns ((:title :link :guid :pubdate :info-hash :category-id
:size :seeders :leechers :trusted :magnet) ...)"
(require 'dom) ; ensure dom-by-tag et al. are loaded
(goto-char (point-min))
(when (re-search-forward "^\r?$" nil t) ; skip HTTP headers
(condition-case err
(let* ((xml (libxml-parse-xml-region (point) (point-max)))
(items (dom-by-tag xml 'item))
results)
(dolist (item items)
(let* ((title (dom-text (car (dom-by-tag item 'title))))
(link (dom-text (car (dom-by-tag item 'link))))
(guid (dom-text (car (dom-by-tag item 'guid))))
(ihash (dom-text (car (dom-by-tag item 'infoHash))))
(title-enc (url-encode-url title))
(magnet (format "magnet:?xt=urn:btih:%s&dn=%s" ihash title-enc)))
(push (list :title title :link link :guid guid
:pubdate (dom-text (car (dom-by-tag item 'pubDate)))
:info-hash ihash
:category-id (dom-text (car (dom-by-tag item 'categoryId)))
:size (dom-text (car (dom-by-tag item 'size)))
:seeders (string-to-number (dom-text (car (dom-by-tag item 'seeders))))
:leechers (string-to-number (dom-text (car (dom-by-tag item 'leechers))))
:trusted (dom-text (car (dom-by-tag item 'trusted)))
:magnet magnet)
results)))
(nreverse results))
(error (message "Nyaa RSS parse error: %s" (error-message-string err))
nil))))
;; ── Results display (tabulated-list) ──
(defvar gortium/nyaa-search-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "RET") #'gortium/nyaa-download-only)
(define-key map "d" #'gortium/nyaa-download-only)
(define-key map "g" #'gortium/nyaa-search)
(define-key map "c" #'gortium/nyaa-copy-magnet)
map)
"Keymap for `gortium/nyaa-search-mode'.")
(define-derived-mode gortium/nyaa-search-mode tabulated-list-mode
"Nyaa-Search"
"Browse Nyaa.si search results.
\\{gortium/nyaa-search-mode-map}"
(setq tabulated-list-format
[("Title" 58 t)
("Size" 10 t :right-align t)
("S" 5 t :right-align t)
("L" 5 t :right-align t)
("Category" 9 t)])
(setq tabulated-list-padding 2)
(setq tabulated-list-sort-key (cons "S" t))
(tabulated-list-init-header)
(setq-local revert-buffer-function #'gortium/nyaa--revert))
(defun gortium/nyaa--revert (&optional _auto _noconfirm)
(when (and (boundp 'gortium/nyaa-search-query) gortium/nyaa-search-query)
(gortium/nyaa-search gortium/nyaa-search-query)))
(defun gortium/nyaa--display-results (results query)
"Fill tabulated-list buffer with RESULTS."
(let* ((buf (get-buffer-create "*Nyaa Search*"))
(entries
(let ((idx 0))
(mapcar
(lambda (r)
(cl-incf idx)
(let* ((title (plist-get r :title))
(disp (truncate-string-to-width title 56 nil nil t)))
(list idx
(vector disp
(plist-get r :size)
(number-to-string (plist-get r :seeders))
(number-to-string (plist-get r :leechers))
(gortium/nyaa-cat-label
(plist-get r :category-id))))))
results))))
;; Switch to current window instead of popping a split
(with-current-buffer buf
(gortium/nyaa-search-mode)
(setq gortium/nyaa-results (cl-mapcar #'cons
(mapcar #'car entries) results))
(setq tabulated-list-entries entries)
(setq gortium/nyaa-search-query query)
(tabulated-list-print)
(switch-to-buffer (current-buffer)))
(message "Nyaa: %d results for \"%s\"" (length results) query)))
;; ── Actions on results ──
(defun gortium/nyaa--result-at-point ()
"Return plist for Nyaa result at point, or nil."
(let ((id (tabulated-list-get-id)))
(and id (cdr (assoc id gortium/nyaa-results)))))
(defun gortium/nyaa-download-only ()
"Add torrent at point to Transmission with sequential download.
Opens the Transmission buffer in a split window to monitor progress."
(interactive)
(let ((r (gortium/nyaa--result-at-point)))
(unless r (user-error "No result at point"))
(gortium/transmission-add-sequential (plist-get r :magnet))
(require 'transmission)
(select-window (split-window-right))
(let ((display-buffer-overriding-action '(display-buffer-same-window)))
(call-interactively #'transmission))
(message "Downloading: %s" (plist-get r :title))))
(defun gortium/nyaa-copy-magnet ()
"Copy magnet URI at point to kill-ring."
(interactive)
(let ((r (gortium/nyaa--result-at-point)))
(unless r (user-error "No result at point"))
(let ((magnet (plist-get r :magnet)))
(kill-new magnet)
(message "Magnet copied: %s" (substring magnet 0 80)))))
;; ══════════════════════════════════════════════════
;; Auto-play (REMOVED — use P in transmission file list)
;; ══════════════════════════════════════════════════
;; ── Transmission: open in current window ──
(defun gortium/transmission ()
"Open Transmission in the current window (not a popup/buffer switch)."
(interactive)
(require 'transmission)
(let ((display-buffer-overriding-action '(display-buffer-same-window)))
(call-interactively #'transmission)))
;; ── Quick-add from a string ──
(defun gortium/nyaa-add-magnet (magnet)
"Add a magnet link to Transmission with sequential download."
(interactive "sMagnet or info hash: ")
(gortium/transmission-add-sequential magnet))
;; Global leader bindings (SPC t prefix for torrent)
(map! :leader
:prefix "t"
:desc "Nyaa search" "n" #'gortium/nyaa-search
:desc "Add magnet" "m" #'gortium/nyaa-add-magnet
:desc "Transmission" "t" #'gortium/transmission)
;; ── Torrent operations menu (simple interactive defun, no transient dependency) ──
(defun gortium/torrent-menu ()
"Torrent operations: search, add, browse."
(interactive)
(let ((key (read-char-choice
"Torrent: (n)yaa (m)agnet add (t)ransmission Display (p)lay-file (c)opy-magnet (d)ownload-only "
'(?n ?m ?t ?p ?c ?d))))
(cl-case key
(?n (call-interactively #'gortium/nyaa-search))
(?m (call-interactively #'gortium/nyaa-add-magnet))
(?t (call-interactively #'gortium/transmission))
(?p (call-interactively #'gortium/transmission-play-file))
(?c (call-interactively #'gortium/nyaa-copy-magnet))
(?d (call-interactively #'gortium/nyaa-download-only)))))
(map! :leader :desc "Torrent menu" "T" #'gortium/torrent-menu)
#+end_src

View File

@@ -3216,3 +3216,299 @@ fallback to today's daily note. Ensures the daily has an Org-roam ID."
;; Make the frame more temporary-like
(set-frame-parameter frame 'delete-before-kill-buffer t)
(set-window-dedicated-p (selected-window) t))))
;;; Torrent to MPV — Nyaa search + Transmission + mpv playback
;; dom.el provides dom-by-tag, dom-text for RSS XML parsing
(require 'dom)
;; ── Transmission daemon config ──
;; NB: no (require 'transmission) here — Doom autoloads it on first use.
(after! transmission
(setq transmission-host "localhost"
transmission-service 9091
transmission-refresh-interval 3
transmission-refresh-modes '(transmission-mode transmission-files-mode)))
;; ── Sequential download (add torrent + play) ──
(defun gortium/transmission-add-sequential (magnet &optional directory callback)
"Add MAGNET to Transmission with sequential download enabled.
If CALLBACK is provided, call it with the torrent name on success."
(interactive "sMagnet: ")
(transmission-request-async
(lambda (response)
(let-alist response
(let ((name (or .torrent-added.name .torrent-duplicate.name)))
(if name
(progn
(message "Added: %s (sequential)" name)
(when callback (funcall callback name)))
(message "Added torrent (sequential)")))))
"torrent-add"
(append `(:filename ,(if (transmission-btih-p magnet)
(concat "magnet:?xt=urn:btih:" magnet)
magnet)
:sequential_download t
:paused :json-false)
(when directory (list :download-dir (expand-file-name directory))))))
;; ── Play file from Transmission file list ──
(defun gortium/transmission-play-file (&optional file)
"Play FILE (or file at point) with mpv overlay.
Works on partial downloads (.part files) too."
(interactive)
(let ((path (or file
(condition-case nil
(transmission-files-file-at-point)
(error nil)))))
(if (null path)
(user-error "No file at point or file not found")
(let ((real-path
(or (and (file-exists-p path) path)
(and (file-exists-p (concat path ".part"))
(concat path ".part"))
;; Check incomplete dir too
(let ((inc (replace-regexp-in-string
"/complete/" "/incomplete/" path)))
(or (and (file-exists-p inc) inc)
(and (file-exists-p (concat inc ".part"))
(concat inc ".part")))))))
(unless real-path
(user-error "File not found: %s" path))
(start-process "transmission-mpv" nil "mpv"
"--quiet" "--really-quiet"
"--geometry=30%x30%-30-50" "--ontop" "--no-border"
"--title=Transmission MPV"
real-path)
(message "Playing: %s" (file-name-nondirectory real-path))))))
;; Override transmission-find-file to always play via mpv instead
;; (advice catches all callers: major mode, Evil, menu, M-x)
(defun gortium/--play-advice (&rest _)
"Redirect find-file to mpv play for partial downloads."
(call-interactively #'gortium/transmission-play-file))
(advice-add 'transmission-find-file :around #'gortium/--play-advice)
;; ══════════════════════════════════════════════════
;; Nyaa.si Search
;; ══════════════════════════════════════════════════
(defvar gortium/nyaa-base-url "https://nyaa.si")
(defvar gortium/nyaa-search-history '())
(defvar-local gortium/nyaa-results nil
"Alist of (ID . plist) for current Nyaa search buffer.")
(defvar gortium/nyaa-category-short
'(("1_1" . "AMV") ("1_2" . "Eng") ("1_3" . "NonEng")
("1_4" . "Raw") ("2_1" . "Flac") ("2_2" . "Lossy")
("3_1" . "L-Eng") ("3_2" . "L-NonE") ("3_3" . "L-Raw")
("4_1" . "LiveEng") ("4_2" . "LiveNEn")("4_3" . "LiveRaw")
("5_1" . "SW-App") ("5_2" . "SW-Game")("6_1" . "XXX"))
"Short category labels for column display.")
(defun gortium/nyaa-cat-label (id)
(or (cdr (assoc id gortium/nyaa-category-short)) id))
(defvar gortium/nyaa-search-query nil
"Last query string, for reverting.")
;; ── Interactive search ──
(defun gortium/nyaa-search (query &optional trusted-only)
"Search Nyaa.si RSS for QUERY, display results in tabulated-list.
With \\[universal-argument], show trusted uploads only."
(interactive
(list (read-string "Nyaa search: " nil 'gortium/nyaa-search-history)
current-prefix-arg))
(setq gortium/nyaa-search-query query)
(let ((url (format "%s/?page=rss&q=%s&s=seeders&o=desc%s"
gortium/nyaa-base-url
(url-encode-url query)
(if trusted-only "&f=1" ""))))
(message "Searching Nyaa for \"%s\"..." query)
(let ((buf (get-buffer-create "*Nyaa Search*")))
(with-current-buffer buf (setq gortium/nyaa-results nil)))
(url-retrieve url #'gortium/nyaa--handle-response (list query))))
;; ── RSS response handler ──
(defun gortium/nyaa--handle-response (status query)
"Handle Nyaa RSS response."
(condition-case err
(let ((results (gortium/nyaa--parse-rss)))
(if (null results)
(progn
(message "Nyaa: no results for \"%s\"" query)
(when (buffer-local-value 'gortium/nyaa-search-query
(get-buffer "*Nyaa Search*"))
(with-current-buffer (get-buffer-create "*Nyaa Search*")
(setq mode-name "Nyaa-Search (no results)"))))
(gortium/nyaa--display-results results query)))
(error (message "Nyaa search error: %S" err))))
(defun gortium/nyaa--parse-rss ()
"Parse Nyaa RSS XML from current buffer into plist list.
Returns ((:title :link :guid :pubdate :info-hash :category-id
:size :seeders :leechers :trusted :magnet) ...)"
(require 'dom) ; ensure dom-by-tag et al. are loaded
(goto-char (point-min))
(when (re-search-forward "^\r?$" nil t) ; skip HTTP headers
(condition-case err
(let* ((xml (libxml-parse-xml-region (point) (point-max)))
(items (dom-by-tag xml 'item))
results)
(dolist (item items)
(let* ((title (dom-text (car (dom-by-tag item 'title))))
(link (dom-text (car (dom-by-tag item 'link))))
(guid (dom-text (car (dom-by-tag item 'guid))))
(ihash (dom-text (car (dom-by-tag item 'infoHash))))
(title-enc (url-encode-url title))
(magnet (format "magnet:?xt=urn:btih:%s&dn=%s" ihash title-enc)))
(push (list :title title :link link :guid guid
:pubdate (dom-text (car (dom-by-tag item 'pubDate)))
:info-hash ihash
:category-id (dom-text (car (dom-by-tag item 'categoryId)))
:size (dom-text (car (dom-by-tag item 'size)))
:seeders (string-to-number (dom-text (car (dom-by-tag item 'seeders))))
:leechers (string-to-number (dom-text (car (dom-by-tag item 'leechers))))
:trusted (dom-text (car (dom-by-tag item 'trusted)))
:magnet magnet)
results)))
(nreverse results))
(error (message "Nyaa RSS parse error: %s" (error-message-string err))
nil))))
;; ── Results display (tabulated-list) ──
(defvar gortium/nyaa-search-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "RET") #'gortium/nyaa-download-only)
(define-key map "d" #'gortium/nyaa-download-only)
(define-key map "g" #'gortium/nyaa-search)
(define-key map "c" #'gortium/nyaa-copy-magnet)
map)
"Keymap for `gortium/nyaa-search-mode'.")
(define-derived-mode gortium/nyaa-search-mode tabulated-list-mode
"Nyaa-Search"
"Browse Nyaa.si search results.
\\{gortium/nyaa-search-mode-map}"
(setq tabulated-list-format
[("Title" 58 t)
("Size" 10 t :right-align t)
("S" 5 t :right-align t)
("L" 5 t :right-align t)
("Category" 9 t)])
(setq tabulated-list-padding 2)
(setq tabulated-list-sort-key (cons "S" t))
(tabulated-list-init-header)
(setq-local revert-buffer-function #'gortium/nyaa--revert))
(defun gortium/nyaa--revert (&optional _auto _noconfirm)
(when (and (boundp 'gortium/nyaa-search-query) gortium/nyaa-search-query)
(gortium/nyaa-search gortium/nyaa-search-query)))
(defun gortium/nyaa--display-results (results query)
"Fill tabulated-list buffer with RESULTS."
(let* ((buf (get-buffer-create "*Nyaa Search*"))
(entries
(let ((idx 0))
(mapcar
(lambda (r)
(cl-incf idx)
(let* ((title (plist-get r :title))
(disp (truncate-string-to-width title 56 nil nil t)))
(list idx
(vector disp
(plist-get r :size)
(number-to-string (plist-get r :seeders))
(number-to-string (plist-get r :leechers))
(gortium/nyaa-cat-label
(plist-get r :category-id))))))
results))))
;; Switch to current window instead of popping a split
(with-current-buffer buf
(gortium/nyaa-search-mode)
(setq gortium/nyaa-results (cl-mapcar #'cons
(mapcar #'car entries) results))
(setq tabulated-list-entries entries)
(setq gortium/nyaa-search-query query)
(tabulated-list-print)
(switch-to-buffer (current-buffer)))
(message "Nyaa: %d results for \"%s\"" (length results) query)))
;; ── Actions on results ──
(defun gortium/nyaa--result-at-point ()
"Return plist for Nyaa result at point, or nil."
(let ((id (tabulated-list-get-id)))
(and id (cdr (assoc id gortium/nyaa-results)))))
(defun gortium/nyaa-download-only ()
"Add torrent at point to Transmission with sequential download.
Opens the Transmission buffer in a split window to monitor progress."
(interactive)
(let ((r (gortium/nyaa--result-at-point)))
(unless r (user-error "No result at point"))
(gortium/transmission-add-sequential (plist-get r :magnet))
(require 'transmission)
(select-window (split-window-right))
(let ((display-buffer-overriding-action '(display-buffer-same-window)))
(call-interactively #'transmission))
(message "Downloading: %s" (plist-get r :title))))
(defun gortium/nyaa-copy-magnet ()
"Copy magnet URI at point to kill-ring."
(interactive)
(let ((r (gortium/nyaa--result-at-point)))
(unless r (user-error "No result at point"))
(let ((magnet (plist-get r :magnet)))
(kill-new magnet)
(message "Magnet copied: %s" (substring magnet 0 80)))))
;; ══════════════════════════════════════════════════
;; Auto-play (REMOVED — use P in transmission file list)
;; ══════════════════════════════════════════════════
;; ── Transmission: open in current window ──
(defun gortium/transmission ()
"Open Transmission in the current window (not a popup/buffer switch)."
(interactive)
(require 'transmission)
(let ((display-buffer-overriding-action '(display-buffer-same-window)))
(call-interactively #'transmission)))
;; ── Quick-add from a string ──
(defun gortium/nyaa-add-magnet (magnet)
"Add a magnet link to Transmission with sequential download."
(interactive "sMagnet or info hash: ")
(gortium/transmission-add-sequential magnet))
;; Global leader bindings (SPC t prefix for torrent)
(map! :leader
:prefix "t"
:desc "Nyaa search" "n" #'gortium/nyaa-search
:desc "Add magnet" "m" #'gortium/nyaa-add-magnet
:desc "Transmission" "t" #'gortium/transmission)
;; ── Torrent operations menu (simple interactive defun, no transient dependency) ──
(defun gortium/torrent-menu ()
"Torrent operations: search, add, browse."
(interactive)
(let ((key (read-char-choice
"Torrent: (n)yaa (m)agnet add (t)ransmission Display (p)lay-file (c)opy-magnet (d)ownload-only "
'(?n ?m ?t ?p ?c ?d))))
(cl-case key
(?n (call-interactively #'gortium/nyaa-search))
(?m (call-interactively #'gortium/nyaa-add-magnet))
(?t (call-interactively #'gortium/transmission))
(?p (call-interactively #'gortium/transmission-play-file))
(?c (call-interactively #'gortium/nyaa-copy-magnet))
(?d (call-interactively #'gortium/nyaa-download-only)))))
(map! :leader :desc "Torrent menu" "T" #'gortium/torrent-menu)

View File

@@ -224,3 +224,6 @@
;; (unpin! pinned-package another-pinned-package)
;; ...Or *all* packages (NOT RECOMMENDED; will likely break things)
;; (unpin! t)
;; Transmission BitTorrent client interface
(package! transmission)