aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--emacs/.emacs.d/init/ycp-basic.el2
-rw-r--r--emacs/.emacs.d/init/ycp-buffer.el6
-rw-r--r--emacs/.emacs.d/init/ycp-client.el3
-rw-r--r--emacs/.emacs.d/init/ycp-editing.el5
-rw-r--r--emacs/.emacs.d/init/ycp-emms.el7
-rw-r--r--emacs/.emacs.d/init/ycp-gnus.el4
-rw-r--r--emacs/.emacs.d/init/ycp-markup.el25
-rw-r--r--emacs/.emacs.d/init/ycp-org.el6
-rw-r--r--emacs/.emacs.d/init/ycp-prog.el1
-rw-r--r--emacs/.emacs.d/init/ycp-reading.el4
-rw-r--r--emacs/.emacs.d/init/ycp-theme.el1
-rw-r--r--emacs/.emacs.d/init/ycp-time.el4
-rw-r--r--emacs/.emacs.d/init/ycp-web.el14
m---------emacs/.emacs.d/lisp/emacs-hnreader0
m---------emacs/.emacs.d/lisp/exitter0
-rw-r--r--emacs/.emacs.d/lisp/my/belf.el193
-rw-r--r--emacs/.emacs.d/lisp/my/emms-info-ytdl.el12
-rw-r--r--emacs/.emacs.d/lisp/my/fediorg.el87
-rw-r--r--emacs/.emacs.d/lisp/my/iarc.el159
-rw-r--r--emacs/.emacs.d/lisp/my/my-buffer.el4
-rw-r--r--emacs/.emacs.d/lisp/my/my-editing.el34
-rw-r--r--emacs/.emacs.d/lisp/my/my-emms.el141
-rw-r--r--emacs/.emacs.d/lisp/my/my-epub.el75
-rw-r--r--emacs/.emacs.d/lisp/my/my-gnus.el23
-rw-r--r--emacs/.emacs.d/lisp/my/my-libgen.el106
-rw-r--r--emacs/.emacs.d/lisp/my/my-mariadb.el66
-rw-r--r--emacs/.emacs.d/lisp/my/my-markup.el26
-rw-r--r--emacs/.emacs.d/lisp/my/my-media-segment.el123
-rw-r--r--emacs/.emacs.d/lisp/my/my-net.el1
-rw-r--r--emacs/.emacs.d/lisp/my/my-nov.el140
-rw-r--r--emacs/.emacs.d/lisp/my/my-org-remark.el65
-rw-r--r--emacs/.emacs.d/lisp/my/my-org.el28
-rw-r--r--emacs/.emacs.d/lisp/my/my-prog.el32
-rw-r--r--emacs/.emacs.d/lisp/my/my-ttrss.el200
-rw-r--r--emacs/.emacs.d/lisp/my/my-utils.el18
-rw-r--r--emacs/.emacs.d/lisp/my/my-web.el77
-rw-r--r--emacs/.emacs.d/lisp/my/my-wget.el21
-rw-r--r--emacs/.emacs.d/lisp/my/my-ytdl.el44
-rw-r--r--emacs/.emacs.d/lisp/my/tor.el57
m---------emacs/.emacs.d/lisp/nov.el0
m---------emacs/.emacs.d/lisp/ttrss.el0
-rw-r--r--manual/singlefile-settings.json185
-rw-r--r--misc/.config/mpv/mpv.conf2
-rw-r--r--misc/.gdbinit5
-rwxr-xr-xmisc/bin/merge-tracks.sh25
-rwxr-xr-xmisc/bin/mpvmix.sh22
-rwxr-xr-xmisc/bin/mv-single-pages.sh3
-rwxr-xr-xmisc/bin/ttrss-fetch.el10
-rwxr-xr-xmisc/bin/unzipall.sh8
-rwxr-xr-xmisc/bin/zipall.sh9
51 files changed, 1889 insertions, 197 deletions
diff --git a/.gitmodules b/.gitmodules
index c762b4a..dc001a5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -124,3 +124,6 @@
[submodule "mastodon.el"]
path = emacs/.emacs.d/lisp/mastodon.el
url = https://codeberg.org/martianh/mastodon.el
+[submodule "emacs/.emacs.d/lisp/ttrss.el"]
+ path = emacs/.emacs.d/lisp/ttrss.el
+ url = https://g.ypei.me/ttrss.el.git
diff --git a/emacs/.emacs.d/init/ycp-basic.el b/emacs/.emacs.d/init/ycp-basic.el
index cb097e7..313004f 100644
--- a/emacs/.emacs.d/init/ycp-basic.el
+++ b/emacs/.emacs.d/init/ycp-basic.el
@@ -62,7 +62,7 @@
(my-package my-utils
(:delay 5)
(my-setq-from-local my-audio-incoming-dir my-video-incoming-dir
- my-document-incoming-dir)
+ my-music-incoming-dir my-document-incoming-dir)
(my-setq-from-local my-copy-file-targets)
(my-keybind global-map
"C-c <f2>" #'my-rename-file-and-buffer
diff --git a/emacs/.emacs.d/init/ycp-buffer.el b/emacs/.emacs.d/init/ycp-buffer.el
index 944a45e..6a560ea 100644
--- a/emacs/.emacs.d/init/ycp-buffer.el
+++ b/emacs/.emacs.d/init/ycp-buffer.el
@@ -50,7 +50,7 @@
(my-configure
(:delay 15)
(my-keybind ctl-x-x-map
- "f" #'follow-mode ; override `font-lock-update'
+ ;; "f" #'follow-mode ; override `font-lock-update'
"r" #'rename-uniquely
"l" #'visual-line-mode)
@@ -98,6 +98,9 @@
(my-package follow
(:delay 15)
+ (require 'my-buffer)
+ ;; Disable follow mode
+ (my-override follow-mode)
;; TODO: update this to adapt to number of windows
(my-keybind follow-mode-map
"C-v" #'follow-scroll-up
@@ -121,6 +124,7 @@
"r" #'next-buffer
"d" nil
"u" nil
+ "." nil
"w" #'kill-ring-save
"i" #'view-mode)
(my-keybind global-map "C-`" #'view-mode))
diff --git a/emacs/.emacs.d/init/ycp-client.el b/emacs/.emacs.d/init/ycp-client.el
index d35898c..16447fd 100644
--- a/emacs/.emacs.d/init/ycp-client.el
+++ b/emacs/.emacs.d/init/ycp-client.el
@@ -91,7 +91,8 @@
(:delay 60)
(require 'my-utils)
(setq my-ytdl-audio-download-dir my-audio-incoming-dir
- my-ytdl-video-download-dir my-video-incoming-dir))
+ my-ytdl-video-download-dir my-video-incoming-dir
+ my-ytdl-music-download-dir my-music-incoming-dir))
(my-package my-media-segment
(:delay 60))
diff --git a/emacs/.emacs.d/init/ycp-editing.el b/emacs/.emacs.d/init/ycp-editing.el
index d497f42..031ae31 100644
--- a/emacs/.emacs.d/init/ycp-editing.el
+++ b/emacs/.emacs.d/init/ycp-editing.el
@@ -31,7 +31,10 @@
(setq-default truncate-lines nil)
(setq kill-do-not-save-duplicates t)
(setq kill-transform-function
- (lambda (s) (when (string-match-p "[^ \t\n]" s) s)))
+ (lambda (s) (when (or
+ (derived-mode-p 'pdf-view-mode)
+ (string-match-p "[^ \t\n]" s))
+ s)))
(setq bidi-inhibit-bpa t)
(setq save-interprogram-paste-before-kill t)
(setq kill-ring-max 200)
diff --git a/emacs/.emacs.d/init/ycp-emms.el b/emacs/.emacs.d/init/ycp-emms.el
index 2b52b17..e49209f 100644
--- a/emacs/.emacs.d/init/ycp-emms.el
+++ b/emacs/.emacs.d/init/ycp-emms.el
@@ -34,6 +34,7 @@
(emms-all)
(setq emms-playing-time-resume-from-last-played t)
(add-to-list 'emms-info-functions 'emms-info-ytdl)
+ (add-to-list 'emms-info-functions 'my-emms-info-ffprobe)
;; emms-info-native is not very useful
(delete 'emms-info-native emms-info-functions)
(setq emms-source-file-default-directory (locate-user-emacs-file "emms"))
@@ -46,6 +47,7 @@
(setq emms-source-file-directory-tree-function
'emms-source-file-directory-tree-find)
(setq emms-info-ytdl-using-torsocks t)
+ (setq emms-info-auto-update nil)
(add-hook 'emms-playlist-mode-hook #'hl-line-mode)
(add-hook 'emms-metaplaylist-mode-hook #'hl-line-mode)
)
@@ -92,12 +94,17 @@
'my-emms-output-current-track-to-i3bar-file)
(add-hook 'emms-player-finished-hook 'my-emms-score-up-playing)
(add-hook 'emms-player-started-hook 'my-emms-score-up-chosen-bonus)
+ (add-hook 'emms-player-started-hook 'my-emms-playlist-maybe-mark-bounds)
+ (add-hook 'emms-player-started-hook 'my-emms-maybe-get-duration-for-current-track)
(setq emms-player-next-function 'my-emms-next-track-or-random-group)
(setq emms-players-preference-f 'my-emms-players-preference)
(my-keybind dired-mode-map "e" #'my-dired-add-to-emms)
(my-override emms-track-simple-description)
(my-emms-add-all)
(my-timer emms-save-scores-timer nil 900 'emms-score-save-hash)
+ (my-override emms-mode-line-playlist-current)
+ (my-override emms-score-show-playing)
+ ;; (my-override emms-playing-time-mode-line)
)
(provide 'ycp-emms)
diff --git a/emacs/.emacs.d/init/ycp-gnus.el b/emacs/.emacs.d/init/ycp-gnus.el
index 9e89ee9..7275363 100644
--- a/emacs/.emacs.d/init/ycp-gnus.el
+++ b/emacs/.emacs.d/init/ycp-gnus.el
@@ -94,7 +94,7 @@
(my-keybind global-map
"C-c n i" #'my-gnus-open-inbox
"C-c n n" #'my-gnus-start
- "C-c n u" #'gnus-group-get-new-news)
+ "C-c n u" #'my-gnus-group-refresh)
(my-server-timer my-gnus-new-news-timer nil 300
'my-gnus-group-get-new-news-quietly)
;; https://superuser.com/questions/519685/gnus-get-rid-of-mail-and-news-folders
@@ -147,6 +147,8 @@
(my-package gnus-group
(require 'my-gnus)
(my-keybind gnus-group-mode-map
+ "g" #'my-gnus-group-refresh
+ "i" #'my-gnus-open-inbox
"n" #'next-line
"p" #'previous-line
"m" #'my-gnus-group-compose
diff --git a/emacs/.emacs.d/init/ycp-markup.el b/emacs/.emacs.d/init/ycp-markup.el
index 6a6e8b8..68b5459 100644
--- a/emacs/.emacs.d/init/ycp-markup.el
+++ b/emacs/.emacs.d/init/ycp-markup.el
@@ -71,7 +71,7 @@
(my-package wiki
(my-keybind wiki-mode-map
"C-'" #'my-wiki-grok-wikipedia)
- (my-setq-from-local wiki-sites)
+ (my-setq-from-local wiki-sites wiki-local-dir)
(wiki-define-site-commands)
(add-to-list 'browse-url-handlers
`(wiki-engine-entry-url-p
@@ -105,7 +105,8 @@
;; No fill, so it requires visual line mode to look nice
(setq nov-text-width t)
(add-hook 'nov-mode-hook 'visual-line-mode)
- (add-hook 'nov-mode-hook 'follow-mode)
+ ;; interfering with dbus
+ ;; (add-hook 'nov-mode-hook 'follow-mode)
(add-hook 'nov-mode-hook (lambda ()
(setq line-spacing .1)))
(add-hook 'nov-post-html-render-hook 'my-nov-set-margins)
@@ -114,7 +115,16 @@
(my-override nov-scroll-up)
(my-keybind nov-mode-map
"Q" #'my-nov-copy-buffer-file-with-staging
- "i" #'imenu)
+ "i" #'imenu
+ "f" #'nov-scroll-up
+ "b" #'nov-scroll-down
+ "F" #'my-nov-skim-forward
+ "B" #'my-nov-skim-backward)
+ (add-to-list 'nov-shr-rendering-functions '(span . my-nov-render-span))
+ (add-to-list 'nov-shr-rendering-functions '(ol . my-nov-render-ol))
+ (add-hook 'nov-mode-hook
+ (lambda ()
+ (add-hook 'post-command-hook #'my-nov-update-mode-line nil t)))
)
;;; json-mode
@@ -125,5 +135,14 @@
(add-hook 'json-mode-hook 'my-json-setup-hook)
)
+(my-package mhtml-mode
+ (my-keybind mhtml-mode-map
+ "C-c C-v" #'my-html-render))
+
+(my-package my-markup
+ (:delay 15)
+ (add-to-list 'auto-mode-alist '("\\.html\\'" . htmlv-mode))
+ )
+
(provide 'ycp-markup)
;;; ycp-markup.el ends here
diff --git a/emacs/.emacs.d/init/ycp-org.el b/emacs/.emacs.d/init/ycp-org.el
index 43ae6cb..77f720d 100644
--- a/emacs/.emacs.d/init/ycp-org.el
+++ b/emacs/.emacs.d/init/ycp-org.el
@@ -306,6 +306,7 @@
(setq org-clock-idle-time 15)
(setq org-clock-mode-line-total 'auto)
(setq org-clock-persist 'history)
+ (setq org-clock-continuously t)
(org-clock-persistence-insinuate))
(my-package org-refile
@@ -375,7 +376,7 @@
("i" . my-org-append-subheading)
("^" . org-sort)
("w" . org-refile)
- ("a" . org-archive-subtree-default-with-confirmation)
+ ("a" . org-archive-subtree-default)
("@" . org-mark-subtree)
("#" . org-toggle-comment)
("Clock Commands")
@@ -528,6 +529,9 @@
(require 'my-org-remark)
(setq org-remark-notes-display-buffer-action
'(display-buffer-reuse-mode-window))
+ (setq org-remark-notes-file-name
+ (locate-user-emacs-file "margin.org"))
+ (my-override org-remark-highlight-add-or-update-highlight-headline)
(require 'nov)
(my-keybind nov-mode-map
"M-n" #'org-remark-next
diff --git a/emacs/.emacs.d/init/ycp-prog.el b/emacs/.emacs.d/init/ycp-prog.el
index 6584491..f74e339 100644
--- a/emacs/.emacs.d/init/ycp-prog.el
+++ b/emacs/.emacs.d/init/ycp-prog.el
@@ -210,6 +210,7 @@
(my-package my-prog
(:delay 10)
(my-keybind global-map "C-c 8" #'my-set-tab-width-to-8)
+ (my-keybind prog-mode-map "C-c M-w" 'my-copy-with-func)
(add-hook 'c-mode-hook 'my-c-set-compile-command)
(define-key c-mode-map (kbd "C-c s") 'my-c-switch-between-header-and-source)
(define-key c++-mode-map (kbd "C-c s")
diff --git a/emacs/.emacs.d/init/ycp-reading.el b/emacs/.emacs.d/init/ycp-reading.el
index 24c47d0..5c0284e 100644
--- a/emacs/.emacs.d/init/ycp-reading.el
+++ b/emacs/.emacs.d/init/ycp-reading.el
@@ -27,6 +27,8 @@
;;; Code:
(my-package belf
- (my-setq-from-local belf-dir))
+ (my-setq-from-local belf-dir belf-locate-dirs)
+ (add-hook 'find-file-hook 'belf-recent-add-current)
+ (blink-cursor-mode 0))
(provide 'ycp-reading)
diff --git a/emacs/.emacs.d/init/ycp-theme.el b/emacs/.emacs.d/init/ycp-theme.el
index ee76311..c6721ed 100644
--- a/emacs/.emacs.d/init/ycp-theme.el
+++ b/emacs/.emacs.d/init/ycp-theme.el
@@ -41,6 +41,7 @@
'normal :weight 'normal :height 150 :width 'normal)
(set-face-attribute 'fixed-pitch nil :family "Ubuntu Mono" :foundry "DAMA"
:slant 'normal :weight 'normal :height 150 :width 'normal)
+(set-face-attribute 'variable-pitch nil :family "Ubuntu" :foundry "DAMA")
(provide 'ycp-theme)
;;; ycp-theme.el ends here
diff --git a/emacs/.emacs.d/init/ycp-time.el b/emacs/.emacs.d/init/ycp-time.el
index f98a9cd..f21061c 100644
--- a/emacs/.emacs.d/init/ycp-time.el
+++ b/emacs/.emacs.d/init/ycp-time.el
@@ -83,7 +83,7 @@
(holiday-fixed 1 26 "Australia Day (Vic holiday)")
(holiday-float 3 1 2 "Labour Day (Vic holiday)")
(holiday-fixed 4 25 "Anzac Day (Vic holiday)")
- (holiday-float 6 1 2 "Monarch's Birthday (Vic oliday)")
+ (holiday-float 6 1 2 "Monarch's Birthday (Vic holiday)")
(holiday-fixed 6 30 "End of financial year")
(holiday-float 9 5 -1 "(Possibly) Friday before the AFL Grand Final (Vic holiday)")
(holiday-float 10 5 1 "(Possibly) Friday before the AFL Grand Final (Vic holiday)")
@@ -123,7 +123,7 @@
(setq appt-display-interval 5)
;; dbus notification of appt
(require 'my-time)
- (setq appt-disp-window-function #'my-app-display-window)
+ (setq appt-disp-window-function #'my-appt-display-window)
;; with org-agenda-to-appt
(require 'org-clock)
(require 'my-utils)
diff --git a/emacs/.emacs.d/init/ycp-web.el b/emacs/.emacs.d/init/ycp-web.el
index 6e939f8..fd16b10 100644
--- a/emacs/.emacs.d/init/ycp-web.el
+++ b/emacs/.emacs.d/init/ycp-web.el
@@ -187,6 +187,7 @@
(my-override hnreader--print-frontpage-item)
(my-override hnreader--print-comments)
(my-override hnreader--get-title)
+ (my-setq-from-local my-hnreader-save-dir)
(require 'my-web)
(add-to-list 'browse-url-handlers
`(my-hacker-news-url-p
@@ -252,6 +253,7 @@
(my-package my-web
(:delay 60)
+ (my-setq-from-local my-webpage-incoming-dir)
(my-keybind eww-mode-map
"N" #'my-eww-next-path
"P" #'my-eww-prev-path
@@ -260,6 +262,7 @@
"b" #'my-eww-switch-by-title)
(my-keybind global-map "\C-c\C-o" #'my-browse-url-at-point)
(my-setq-from-local my-newscorp-au-amp-nk)
+ (my-setq-from-local my-tor-browser-bin)
(add-to-list 'browse-url-handlers
`(my-newscorp-au-url-p
. ,(lambda (url &rest _) (my-open-newscorp-au url))))
@@ -338,6 +341,7 @@
(my-package fediorg
(:delay 60)
+ (my-setq-from-local fediorg-dir)
(require 'my-web)
(add-to-list 'browse-url-handlers
`(fediorg-post-url-p
@@ -387,18 +391,26 @@
exitter-oauth-consumer-key exitter-oauth-consumer-secret
exitter-access-token exitter-username exitter-password exitter-email
exitter-oauth-token exitter-oauth-token-secret exitter-oauth-token-ctime)
+ (my-setq-from-local exitter-dir)
(setq exitter-debug nil)
(add-to-list 'browse-url-handlers
`(exitter-post-url-p
- . ,(lambda (url &rest _) (exitter-open-post url))))
+ . ,(lambda (url arg) (exitter-open-post url arg))))
)
(my-package reddio
(:delay 60)
+ (my-setq-from-local reddio-dir)
(add-to-list 'browse-url-handlers
`(reddio-reddit-url-p
. ,(lambda (url &rest _) (reddio-open-url url))))
)
+(my-package ttrss
+ (:delay 60)
+ (my-setq-from-local ttrss-address ttrss-user ttrss-password)
+ (require 'my-ttrss)
+ (my-setq-from-local my-ttrss-dir))
+
(provide 'ycp-web)
diff --git a/emacs/.emacs.d/lisp/emacs-hnreader b/emacs/.emacs.d/lisp/emacs-hnreader
-Subproject 8444e177035e236e991f9ea73074c053a45426a
+Subproject a56f67a99a855ca656da1c1985e09f44509e4bb
diff --git a/emacs/.emacs.d/lisp/exitter b/emacs/.emacs.d/lisp/exitter
-Subproject 7ccd8ff06b50008ad0602c6652caebd4c4674a6
+Subproject ed4f8948ac275c4381daa2aeda1d97df5fedcba
diff --git a/emacs/.emacs.d/lisp/my/belf.el b/emacs/.emacs.d/lisp/my/belf.el
index 35175be..df9b53b 100644
--- a/emacs/.emacs.d/lisp/my/belf.el
+++ b/emacs/.emacs.d/lisp/my/belf.el
@@ -28,6 +28,7 @@
(require 'tabulated-list)
(require 'infobox)
+(require 'my-epub)
(defvar-keymap belf-mode-map
:parent tabulated-list-mode-map
@@ -76,7 +77,7 @@
(defvar belf-dir "~/Documents" "Directory of books.")
-(defun belf-parse-all-file-names ()
+(defun belf-parse-file-names (file-names)
(seq-filter
#'identity
(seq-map
@@ -84,7 +85,10 @@
(when-let ((parsed (belf-parse-file-name f)))
(let-alist parsed
(list f (vector .authors .title .year)))))
- (directory-files belf-dir t "\\.\\(epub\\|pdf\\|cbr\\|djvu\\|mobi\\)$"))))
+ file-names)))
+
+(defun belf-parse-all-file-names ()
+ (belf-parse-file-names (directory-files belf-dir t "\\.\\(epub\\|pdf\\|cbr\\|djvu\\|mobi\\|azw3\\)$")))
(defun belf-file-name-desort (file-name new-dir)
"Rename a file.
@@ -147,47 +151,21 @@ foo bar & quux, baf"
(belf-epub-rename epub new-dir)))
(defun belf-epub-rename (file-name new-dir)
- (when-let ((content-file-name (belf-epub-content-file-name file-name)))
- (with-temp-buffer
- (call-process "unzip" nil t nil "-p" file-name content-file-name)
- (let* ((dom (libxml-parse-xml-region (point-min) (point-max)))
- (metadata (dom-by-tag dom 'metadata))
- (title (dom-text (dom-by-tag metadata 'title)))
- (authors (dom-texts (dom-by-tag metadata 'creator) ", "))
- (identifier
- (replace-regexp-in-string
- "[^0-9,]" ""
- (dom-texts
- (seq-filter
- (lambda (node)
- (or (equal "ISBN" (dom-attr node 'scheme))
- (string-match-p "^[0-9]+$" (dom-text node))))
- (dom-by-tag metadata 'identifier))
- ",")))
- (date (replace-regexp-in-string
- "[^0-9]" ""
- (dom-text (dom-by-tag metadata 'date))))
- (year (substring date 0 (min 4 (length date))))
- (dir (file-name-directory file-name))
- (new-base-name (belf-format-base-name
- `((title . ,title)
- (authors . ,authors)
- (year . ,year)
- (identifier . ,identifier))
- new-dir))
- new-name)
- ;; (pp metadata)
- (dolist (file (directory-files dir t
- (format "^%s\\.[a-zA-Z0-9]+$"
- (regexp-quote
- (file-name-base file-name)))))
- (setq new-name (format "%s.%s" new-base-name (file-name-extension file)))
- (unless (equal file-name new-name)
- (message "%s -> %s" file new-name)
- (ignore-error 'file-already-exists (rename-file file new-name))
- )
+ (when-let ((meta (my-epub-metadata file-name)))
+ (let* ((dir (file-name-directory file-name))
+ (new-base-name (belf-format-base-name meta new-dir))
+ new-name)
+ (dolist (file (directory-files dir t
+ (format "^%s\\.[a-zA-Z0-9]+$"
+ (regexp-quote
+ (file-name-base file-name)))))
+ (setq new-name (format "%s.%s" new-base-name (file-name-extension file)))
+ (unless (equal file-name new-name)
+ (message "%s -> %s" file new-name)
+ (ignore-error 'file-already-exists (rename-file file new-name))
)
- ))
+ )
+ )
))
(defun belf-move-invalid-file-names (dir new-dir)
@@ -201,6 +179,14 @@ foo bar & quux, baf"
(rename-file file-name new-name)
))))
+(defun belf-dired-do-epub-rename ()
+ (interactive)
+ (seq-do
+ (lambda (file)
+ (when (equal (upcase (file-name-extension file)) "EPUB")
+ (belf-epub-rename file (file-name-directory file))))
+ (dired-get-marked-files)))
+
(defun belf-epub-rename-at-point ()
(interactive)
(let ((file-name (tabulated-list-get-id)))
@@ -242,15 +228,6 @@ foo bar & quux, baf"
(json-read)))
)
-(defun belf-epub-content-file-name (file-name)
- (with-temp-buffer
- (if (eq 0 (call-process "unzip" nil t nil
- "-p" file-name "META-INF/container.xml"))
- (let ((dom (libxml-parse-xml-region (point-min) (point-max))))
- (dom-attr (dom-by-tag (dom-by-tag (dom-by-tag dom 'container) 'rootfiles) 'rootfile) 'full-path))
- (message "Failed to extract container.xml: %s" (buffer-string))
- nil)))
-
(defun belf-epub-cover-file-name (file-name content-file-name)
(with-temp-buffer
(call-process "unzip" nil t nil "-p" file-name content-file-name)
@@ -326,7 +303,7 @@ For EPUB, looks for a cover image in the file."
(if-let ((cover-file-name (belf-locate-book-cover file-name)))
(concat "file://" cover-file-name)
(cond ((equal "epub" (file-name-extension file-name))
- (when-let* ((content-file-name (belf-epub-content-file-name file-name))
+ (when-let* ((content-file-name (my-epub-content-file-name file-name))
(cover-file
(belf-epub-cover-file-name file-name content-file-name))
(cover-file-name (file-name-with-extension
@@ -352,12 +329,13 @@ For EPUB, looks for a cover image in the file."
(alist-get 'authors (belf-parse-file-name
(tabulated-list-get-id))))))
(let* ((file-name (tabulated-list-get-id))
+ (dir (file-name-directory file-name))
(parsed (belf-parse-file-name file-name))
new-base-name
new-file)
(setf (alist-get 'authors parsed) new-authors)
- (setq new-base-name (belf-format-base-name parsed))
- (dolist (file (directory-files belf-dir t
+ (setq new-base-name (belf-format-base-name parsed dir))
+ (dolist (file (directory-files dir t
(format "^%s\\.[a-zA-Z0-9]+$"
(regexp-quote
(file-name-base file-name)))))
@@ -448,4 +426,111 @@ Compare without leading \"The \"."
(interactive)
(find-file-other-window (tabulated-list-get-id)))
+;;; belf-recent
+
+(defvar belf-recent-file (locate-user-emacs-file "belf-list"))
+
+(defun belf-recent-add (file)
+ "Add FILE to `belf-recent-file'.
+
+Can be used as a `find-file-hook'."
+ (when (string-match-p "\\.\\(epub\\|pdf\\|cbr\\|djvu\\|mobi\\|azw3\\)$"
+ file)
+ (with-temp-buffer
+ (when (file-exists-p belf-recent-file)
+ (insert-file-contents belf-recent-file))
+ (goto-char (point-min))
+ (flush-lines (rx-to-string `(and bol "[" (= 23 anychar) "] " ,file eol)))
+ (insert
+ (format-time-string "[%Y-%m-%d %a %H:%M:%S]" (current-time))
+ " "
+ file
+ "\n")
+ (write-file belf-recent-file)
+ )))
+
+(defun belf-recent-add-current ()
+ (when buffer-file-name
+ (belf-recent-add buffer-file-name)))
+
+(define-derived-mode belf-recent-mode belf-mode "Bookshelf Recent"
+ "Major mode for browsing a list of books."
+ (setq revert-buffer-function #'belf-recent-list-refresh-contents))
+
+(defun belf-recent ()
+ (interactive)
+ (let ((buf (get-buffer-create "*Bookshelf Recent*")))
+ (with-current-buffer buf
+ (belf-recent-mode)
+ (belf-recent-list-refresh-contents))
+ (pop-to-buffer-same-window buf)))
+
+;; (defvar belf-find-dir nil
+;; "Directory to run find command for relocated files.")
+
+(defvar belf-locate-dirs nil
+ "Directories to look for relocated files.")
+
+(defun belf-recent-bookkeeping ()
+ "Check `belf-recent-file' for (re)moved files and update accordingly."
+ (interactive)
+ (copy-file belf-recent-file (concat belf-recent-file ".bak") t)
+ (with-temp-buffer
+ (when (file-exists-p belf-recent-file)
+ (insert-file-contents belf-recent-file))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (forward-char 26)
+ (let* ((beg (point))
+ (end (progn (end-of-line) (point)))
+ (file-name (buffer-substring-no-properties beg end)))
+ (unless (file-exists-p file-name)
+ (let ((dirs belf-locate-dirs)
+ (file-name-nodir (file-name-nondirectory file-name))
+ dir new-name found)
+ (delete-region beg end)
+ (while (and (not found) dirs)
+ (setq dir (expand-file-name (car dirs))
+ new-name (file-name-concat dir file-name-nodir)
+ found (file-exists-p new-name)
+ dirs (cdr dirs)))
+ (when found (insert new-name)))
+ ;; Running find on a big dir is too slow even when there are
+ ;; only a few thousands subdirs
+ ;; (call-process "find" nil (current-buffer) nil
+ ;; (expand-file-name belf-find-dir)
+ ;; "-name" (file-name-nondirectory file-name))
+ )
+ (beginning-of-line 2)))
+
+ ;; Remove empty records that could not be found
+ (goto-char (point-min))
+ (flush-lines (rx bol (= 26 anychar) eol))
+
+ ;; Deduplicate
+ (goto-char (point-min))
+ (while (not (eobp))
+ (forward-char 26)
+ (let* ((beg (point))
+ (end (progn (end-of-line) (point)))
+ (file-name (buffer-substring-no-properties beg end)))
+ (flush-lines
+ (rx-to-string `(and bol "[" (= 23 anychar) "] " ,file-name eol))))
+ (beginning-of-line 2))
+ (write-file belf-recent-file)))
+
+(defun belf-recent-list-refresh-contents (&rest _)
+ (belf-recent-bookkeeping)
+ (setq-local tabulated-list-entries (belf-recent-parse-file-names))
+ (tabulated-list-print))
+
+(defun belf-recent-parse-file-names ()
+ (with-temp-buffer
+ (when (file-exists-p belf-recent-file)
+ (insert-file-contents belf-recent-file))
+ (goto-char (point-min))
+ (replace-regexp (rx bol (= 26 anychar)) "")
+ (belf-parse-file-names (string-lines (buffer-string))))
+ )
+
(provide 'belf)
diff --git a/emacs/.emacs.d/lisp/my/emms-info-ytdl.el b/emacs/.emacs.d/lisp/my/emms-info-ytdl.el
index 489f3fb..0c7a1d2 100644
--- a/emacs/.emacs.d/lisp/my/emms-info-ytdl.el
+++ b/emacs/.emacs.d/lisp/my/emms-info-ytdl.el
@@ -31,7 +31,7 @@
(require 'emms-info)
(require 'json)
-
+(require 'tor)
(defgroup emms-info-ytdl nil
"Options for EMMS."
@@ -70,12 +70,10 @@
(with-temp-buffer
(when (zerop
(let ((coding-system-for-read 'utf-8))
- (if emms-info-ytdl-using-torsocks
- (my-call-process-with-torsocks
- emms-info-ytdl-command nil '(t nil) nil "-j"
- (emms-track-name track))
- (call-process emms-info-ytdl-command nil '(t nil) nil
- "-j" (emms-track-name track)))))
+ (my-call-process-with-torsocks
+ (not emms-info-ytdl-using-torsocks)
+ emms-info-ytdl-command nil '(t nil) nil "-j"
+ (emms-track-name track))))
(goto-char (point-min))
(condition-case nil
(let ((json-fields (json-read)))
diff --git a/emacs/.emacs.d/lisp/my/fediorg.el b/emacs/.emacs.d/lisp/my/fediorg.el
index e2f21b8..744206e 100644
--- a/emacs/.emacs.d/lisp/my/fediorg.el
+++ b/emacs/.emacs.d/lisp/my/fediorg.el
@@ -233,11 +233,14 @@ Including ancestors and descendants, if any."
attachments
"\n"))
-(defun fediorg-format-post (post level)
- "Format a POST with indent LEVEL."
+(defun fediorg-format-post (post level &optional absolute-time)
+ "Format a POST with indent LEVEL.
+
+Required fields: url, created_at, account.username
+Optional fields: account.display_name"
(let-alist post
(let ((host (car (fediorg-parse-url .url))))
- (format "%s %s (@%s@%s) %s\n\n%s%s\n\n⤷%d ⇆%d ★%d\n"
+ (format "%s %s (@%s@%s) %s\n\n%s%s\n\n⤷%s ⇆%s ★%s\n"
(make-string level ?*)
(if (string-empty-p .account.display_name)
.account.username .account.display_name)
@@ -245,10 +248,12 @@ Including ancestors and descendants, if any."
host
(fediorg-make-org-link
.url
- (fediorg--relative-time-description .created_at))
+ (if absolute-time .created_at
+ (fediorg--relative-time-description .created_at)))
(with-temp-buffer
(insert .content)
- (shr-render-region (point-min) (point-max))
+ (let ((shr-fill-text nil))
+ (shr-render-region (point-min) (point-max)))
(buffer-substring-no-properties (point-min) (point-max)))
(fediorg-format-attached .media_attachments host)
.replies_count
@@ -275,6 +280,78 @@ Including ancestors and descendants, if any."
(pcase-let ((`(,host . ,post-id) (fediorg-parse-url url)))
(format "%s.%s.org" host post-id)))
+(defun fediorg-archive-make-file-name (actor)
+ (let-alist (fediorg-archive-parse-actor actor)
+ (format "%s.%s.org" .host .username)))
+
+(defun fediorg-archive-parse-actor (actor)
+ "Parse actor to username and display_name."
+ (let ((username (alist-get 'preferredUsername actor))
+ (name (alist-get 'name actor))
+ (url (alist-get 'id actor)))
+ (pcase-let* ((urlobj (url-generic-parse-url url))
+ (host (url-host urlobj)))
+ `((display_name . ,name)
+ (username . ,username)
+ (host . ,host))))
+ )
+
+(defun fediorg-archive-format-item (item actor)
+ "ACTOR is parsed actor.json."
+ (let (post)
+ (pcase (alist-get 'type item)
+ ("Create"
+ (setq post (alist-get 'object item))
+ (setf (alist-get 'url post) (alist-get 'id post)
+ (alist-get 'created_at post) (alist-get 'published item)
+ (alist-get 'account post) actor
+ (alist-get 'replies_count post) (alist-get 'repliesCount post)))
+ ("Announce"
+ (setf (alist-get 'url post) (alist-get 'object item)
+ (alist-get 'created_at post) (alist-get 'published item)
+ (alist-get 'account post) actor
+ (alist-get 'content post)
+ (format "[repost of %s]" (alist-get 'object item)))))
+ (fediorg-format-post post 1 t)))
+
+(defun fediorg-archive-format-outbox (info actor)
+ "Transform an outbox.json and an actor.json to an org file."
+ (let ((parsed-actor (fediorg-archive-parse-actor actor)))
+ (string-join
+ (seq-map
+ (lambda (item) (fediorg-archive-format-item item parsed-actor))
+ (alist-get 'orderedItems info))
+ "\n")))
+
+;;;###autoload
+(defun fediorg-archive-open (dir)
+ "Given a fedi archive, open an org buffer showing all outbox posts.
+
+A fedi archive can be obtained by exporting. This function requires an
+outbox.json and an actor.json file.
+
+TODO:
+- add support for likes and bookmarks
+- add support for inline announces
+- add inline images
+- mark dm
+"
+ (interactive (list (read-directory-name "Fedi archive dir: ")))
+ (let ((outbox-file (file-name-concat dir "outbox.json"))
+ (actor-file (file-name-concat dir "actor.json"))
+ (outbox) (actor))
+ (unless (and (file-exists-p actor-file) (file-exists-p outbox-file))
+ (error "Actor or outbox file missing!"))
+ (with-temp-buffer
+ (insert-file-contents actor-file)
+ (setq actor (json-read)))
+ (with-temp-buffer
+ (insert-file-contents outbox-file)
+ (setq outbox (json-read)))
+ (fediorg-save-text-and-switch-to-buffer
+ (fediorg-archive-format-outbox outbox actor)
+ (file-name-concat dir (fediorg-archive-make-file-name actor)))))
+
;;;###autoload
(defun fediorg-open (url)
"Given a fedi post URL, open an org buffer rendering the post.
diff --git a/emacs/.emacs.d/lisp/my/iarc.el b/emacs/.emacs.d/lisp/my/iarc.el
new file mode 100644
index 0000000..d29d525
--- /dev/null
+++ b/emacs/.emacs.d/lisp/my/iarc.el
@@ -0,0 +1,159 @@
+;;; iarc.el -- internet archive client -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; Package-Requires: ((emacs "30.1"))
+
+;; This file is part of dotted.
+
+;; dotted is free software: you can redistribute it and/or modify it under
+;; the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; dotted is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+;; Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public
+;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; internet archive client.
+
+;;; Code:
+
+(require 'infobox)
+
+(defvar-keymap iarc-mode-map
+ :parent tabulated-list-mode-map
+ "F" #'iarc-toggle-follow-mode
+ "i" #'iarc-infobox
+ "n" #'iarc-next-line
+ "p" #'iarc-previous-line
+ "RET" #'iarc-item-at-point
+ )
+
+(define-derived-mode iarc-mode tabulated-list-mode "IArc"
+ (hl-line-mode 1)
+ (setq tabulated-list-format
+ [("★ " 3 iarc-compare-favourites :right-align t)
+ ("Title" 60 t)])
+ (setq tabulated-list-padding 2)
+ (tabulated-list-init-header)
+ (setq revert-buffer-function #'iarc-list-refresh))
+
+(defvar iarc-search-dataset nil)
+
+(defvar iarc-follow-mode nil "Whether follow mode is on.")
+
+(defun iarc-toggle-follow-mode ()
+ (interactive)
+ (setq iarc-follow-mode (not iarc-follow-mode)))
+
+(defun iarc-previous-line ()
+ (interactive)
+ (previous-line)
+ (when iarc-follow-mode
+ (iarc-infobox)))
+
+(defun iarc-next-line ()
+ (interactive)
+ (next-line)
+ (when iarc-follow-mode
+ (iarc-infobox)))
+
+(defun iarc-compare-favourites (x y)
+ (> (let-alist (car x) (or .fields.num_favorites 0))
+ (let-alist (car y) (or .fields.num_favorites 0))))
+
+(defun iarc-list-print-entry (info)
+ (let-alist (alist-get 'fields info)
+ (list info (vector (format "%d" (or .num_favorites 0))
+ .title))))
+
+(defun iarc-list-refresh (&rest _)
+ (interactive)
+ (setq tabulated-list-entries
+ (seq-map 'iarc-list-print-entry iarc-search-dataset))
+ (tabulated-list-print))
+
+(defun iarc ()
+ (let ((buf (get-buffer-create "*IArc*")))
+ (with-current-buffer buf
+ (iarc-mode)
+ (iarc-list-refresh))
+ (pop-to-buffer-same-window buf)))
+
+(defvar iarc-host "https://archive.org")
+
+(defun iarc-api-search (query)
+ (my-url-fetch-json
+ (format "%s/services/search/beta/page_production/?user_query=title:(%s)&hits_per_page=100&page=1&aggregations=false"
+ iarc-host query)))
+
+(defun iarc-search (query)
+ (interactive "sIArc Query: ")
+ (setq iarc-search-dataset (let-alist (iarc-api-search query)
+ .response.body.hits.hits))
+ (iarc))
+
+(defun iarc-infobox ()
+ (interactive)
+ (let ((help-window-select (not iarc-follow-mode)))
+ (iarc-render-info (alist-get 'fields (tabulated-list-get-id)))))
+
+(defun iarc-render-info (info)
+ (infobox-render
+ (infobox-translate info (infobox-default-specs info))
+ `(iarc-render-infobox ,info)
+ (called-interactively-p 'interactive)))
+
+(defun iarc-item-at-point ()
+ (interactive)
+ (iarc-item (alist-get 'identifier (alist-get 'fields
+ (tabulated-list-get-id)))))
+
+(define-derived-mode iarc-item-mode tabulated-list-mode "IArc Item"
+ (hl-line-mode 1)
+ (setq revert-buffer-function #'iarc-list-refresh))
+
+(defvar-local iarc-item-id nil "The item identifier for the iarc-item mode")
+(defvar-local iarc-item-data nil "The content of the iarc-item mode")
+
+(defun iarc-item (id)
+ "List content of item with ID"
+ (let* ((buf (get-buffer-create (format "*IArc %s*" id)))
+ (out
+ (seq-map
+ (lambda (s) (split-string s "\t"))
+ (string-lines
+ (with-temp-buffer
+ (call-process "ia" nil t nil "ls" "-v" "-c" "name,mtime,size" id)
+ (buffer-string))))))
+ (with-current-buffer buf
+ (setq tabulated-list-format
+ (vconcat
+ (seq-map
+ (lambda (c) (list c 20))
+ (car out))))
+ (setq tabulated-list-padding 2)
+ (iarc-item-mode)
+ (tabulated-list-init-header)
+ (setq iarc-item-data (cdr out))
+ (iarc-item-list-refresh))
+ (pop-to-buffer-same-window buf)))
+
+(defun iarc-item-list-refresh ()
+ (setq tabulated-list-entries
+ (seq-map 'iarc-item-list-print-entry iarc-item-data))
+ (tabulated-list-print))
+
+(defun iarc-item-list-print-entry (info)
+ (list info (vconcat info)))
+
+(provide 'iarc)
+;;; iarc.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-buffer.el b/emacs/.emacs.d/lisp/my/my-buffer.el
index a8683de..1c93abc 100644
--- a/emacs/.emacs.d/lisp/my/my-buffer.el
+++ b/emacs/.emacs.d/lisp/my/my-buffer.el
@@ -526,5 +526,9 @@ With double prefix arguments, create a new indirect buffer."
(with-no-warnings (font-lock-fontify-buffer)))
(buffer-string)))
+;;; Disable follow mode
+(defun my-follow-mode (&rest _)
+ (error "follow-mode is disabled."))
+
(provide 'my-buffer)
;;; my-buffer.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-editing.el b/emacs/.emacs.d/lisp/my/my-editing.el
index 0775063..8ce68dd 100644
--- a/emacs/.emacs.d/lisp/my/my-editing.el
+++ b/emacs/.emacs.d/lisp/my/my-editing.el
@@ -189,7 +189,10 @@ by passing optional prefix ARG (\\[universal-argument])."
(beginning-of-line)
(newline)
(forward-line -1)
- (indent-according-to-mode))
+ ;; `indent-according-to-mode' causes cursor to jump to the
+ ;; beginning of an org src block
+ (unless (and (derived-mode-p 'org-mode) (org-in-src-block-p))
+ (indent-according-to-mode)))
(forward-line -1)
(my-new-line-below))))
@@ -528,7 +531,7 @@ With an prefix-arg, copy the file name relative to project root."
(interactive)
(let ((old-max (point-max))
(old-point (point)))
- (comment-kill (or n 1))
+ (when comment-start (comment-kill (or n 1)))
(when (= old-max (point-max))
(goto-char old-point)
(kill-sexp n))))
@@ -546,11 +549,32 @@ With an prefix-arg, copy the file name relative to project root."
(defun my-elide-region (b e)
(interactive "r")
- (let ((message-elide-ellipsis (concat comment-start
- " [... %l lines elided]
-")))
+ (let ((message-elide-ellipsis
+ (if (> (count-lines b (min (1+ e) (point-max))) 1)
+ (concat comment-start
+ " [... %l lines elided]
+")
+ (format " [... %d words elided]" (count-words b e)))))
(message-elide-region b e)))
+(defun my-elide-text (text limit)
+ "Elide TEXT to about LIMIT characters."
+ (let ((keep (- limit 25)))
+ (when (< keep 0)
+ (error "Too few characters to limit to. Should be at least 25."))
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (while (and (<= (point) keep) (< (point) (point-max)))
+ (forward-word))
+ (cond ((> (point) keep)
+ (backward-word)
+ (my-elide-region (point) (point-max))
+ (buffer-string))
+ (t text))
+ ))
+ )
+
(defun my-replace-no-filter (old-fun &rest r)
(let ((search-invisible t))
(apply old-fun r)))
diff --git a/emacs/.emacs.d/lisp/my/my-emms.el b/emacs/.emacs.d/lisp/my/my-emms.el
index e77089d..0a42efe 100644
--- a/emacs/.emacs.d/lisp/my/my-emms.el
+++ b/emacs/.emacs.d/lisp/my/my-emms.el
@@ -257,8 +257,7 @@ filter extensions from filter-exts."
(not (equal s ""))
(or (not filter-exts)
(member
- (when (string-match "^.*\\.\\(.*\\)$" s)
- (match-string 1 s))
+ (downcase (or (file-name-extension s) ""))
filter-exts))))
(split-string
(buffer-substring-no-properties from to) "
@@ -464,26 +463,88 @@ under /zzz-seren/."
(min end-ln maybe-group-end))))
(cons group-start group-end))))
+(defvar-local my-emms-playlist-group-start-overlay nil)
+(defvar-local my-emms-playlist-group-end-overlay nil)
+
+(defun my-emms-playlist-mark-bounds (group-end)
+ "Mark bounds of the current track group.
+
+An up arrow at the first played in the current group, and a down
+arrow at the end of the track group."
+ (when my-emms-playlist-group-start-overlay
+ (delete-overlay my-emms-playlist-group-start-overlay))
+ (when my-emms-playlist-group-start-overlay
+ (delete-overlay my-emms-playlist-group-end-overlay))
+ (setq my-emms-playlist-group-start-overlay (make-overlay (point) (point)))
+ (overlay-put
+ my-emms-playlist-group-start-overlay
+ 'before-string (propertize
+ "x" 'display
+ `(left-fringe up-arrow emms-playlist-selected-face)))
+ (save-excursion
+ (goto-line (1- group-end))
+ (setq my-emms-playlist-group-end-overlay (make-overlay (point) (point)))
+ (overlay-put
+ my-emms-playlist-group-end-overlay
+ 'before-string (propertize
+ "x" 'display
+ `(left-fringe down-arrow emms-playlist-selected-face)))))
+
+(defun my-emms-mode-line-playlist-current ()
+ "Format the currently playing song.
+
+Override `emms-mode-line-playlist-current' to incorporate wide chars."
+ (let ((track-desc (my-emms-get-display-name-1
+ (emms-track-description
+ (emms-playlist-current-selected-track)))))
+ (format emms-mode-line-format
+ (if (< (string-width track-desc) emms-mode-line-length-limit)
+ track-desc
+ (concat
+ (seq-subseq
+ track-desc 0
+ (- (length track-desc)
+ (- (string-width track-desc) emms-mode-line-length-limit)))
+ "...")))))
+
+
+;; (defun my-emms-playing-time-mode-line ()
+;; "Add playing time to the mode line.
+
+;; Override `emms-playing-time-mode-line': prepend instead of append."
+;; (or global-mode-string (setq global-mode-string '("")))
+;; (unless (member 'emms-playing-time-string
+;; global-mode-string)
+;; (setq global-mode-string
+;; (append '(emms-playing-time-string) global-mode-string))))
+
+
(defun my-emms-playlist-random-group ()
(interactive)
(with-current-emms-playlist
- (remove-overlays)
- (goto-line (1+ (random (count-lines (point-min) (point-max)))))
- (pcase-let ((`(,group-start . ,group-end) (my-emms-playlist-group-bounds)))
- (goto-line group-start)
- (overlay-put
- (make-overlay (point) (point))
- 'before-string (propertize
- "x" 'display
- `(left-fringe down-arrow emms-playlist-selected-face)))
- (save-excursion
- (goto-line (1- group-end))
- (overlay-put
- (make-overlay (point) (point))
- 'before-string (propertize
- "x" 'display
- `(left-fringe up-arrow emms-playlist-selected-face))))
- (emms-playlist-mode-play-current-track))))
+ (let ((random-line (1+ (random (count-lines (point-min) (point-max))))))
+ (goto-line random-line)
+ (pcase-let ((`(,group-start . ,group-end) (my-emms-playlist-group-bounds)))
+ (message "my-emms-playlist-random-group: (%d, %d)" random-line group-start)
+ (goto-line group-start)
+ (my-emms-playlist-mark-bounds group-end)
+ (emms-playlist-mode-play-current-track)))))
+
+;;; TODO: mark bounds if and only if the currently played is out of
+;;; the existing overlay.
+(defun my-emms-playlist-maybe-mark-bounds ()
+ "Used as an `emms-player-started-hook'.
+
+If the last command is `emms-playlist-mode-play-smart' i.e. the
+user manually chose the track to play, and if
+`emms-player-next-function' is
+`my-emms-next-track-or-random-group', then mark boundaries since
+it would not have been marked otherwise."
+ (when (and (eq last-command 'emms-playlist-mode-play-smart)
+ (eq emms-player-next-function 'my-emms-next-track-or-random-group))
+ (with-current-emms-playlist
+ (pcase-let ((`(_ . ,group-end) (my-emms-playlist-group-bounds)))
+ (my-emms-playlist-mark-bounds group-end)))))
(defun my-emms-next-track-or-random-group ()
(interactive)
@@ -563,12 +624,22 @@ character."
(defvar my-emms-score-delta 1)
(defun my-emms-score-up-playing ()
- "Increase score by `my-emms-score-delta', then reset it to 1."
+ "Increase score by `my-emms-score-delta', then reset the score delta to 1."
(emms-score-change-score
my-emms-score-delta
(my-emms-get-display-name-1 (emms-score-current-selected-track-filename)))
(setq my-emms-score-delta 1))
+(defun my-emms-score-show-playing ()
+ "Show score for current playing track in minibuf.
+
+Override `emms-score-show-playing' - using last three components in the name..."
+ (interactive)
+ (message "track/tolerance score: %d/%d"
+ (emms-score-get-score (my-emms-get-display-name-1
+ (emms-score-current-selected-track-filename)))
+ emms-score-min-score))
+
(defun my-emms-score-up-chosen-bonus ()
"Bonus score up if the track is started intentionally.
@@ -581,14 +652,40 @@ If the last command is `emms-playlist-mode-play-smart', then set
)
(defun my-emms-wrapped ()
- "Print top 5 scored tracks."
+ "Print top 10 scored tracks."
(interactive)
(let (keys)
(maphash (lambda (k _) (push k keys)) emms-score-hash)
(sort keys (lambda (k1 k2)
(> (cl-second (gethash k1 emms-score-hash))
(cl-second (gethash k2 emms-score-hash)))))
- (message "Top 5: %s" (string-join (take 5 keys) "\n"))))
+ (message "Top 10: %s" (string-join (take 10 keys) "\n"))))
+
+(defun my-emms-maybe-get-duration-for-current-track ()
+ "Get duration for the current track.
+
+Can be used as a `emms-player-started-hook'"
+ (unless (emms-track-get (emms-playlist-current-selected-track)
+ 'info-playing-time)
+ (my-emms-info-ffprobe (emms-playlist-current-selected-track))))
+
+(defun my-emms-info-ffprobe (track)
+ "Use ffprobe for urls to get duration.
+
+Call
+
+ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1
+
+on the url"
+ (when (eq (emms-track-type track) 'url)
+ (with-temp-buffer
+ (call-process "ffprobe" nil t nil "-v" "error" "-show_entries"
+ "format=duration" "-of" "default=noprint_wrappers=1:nokey=1"
+ (emms-track-name track))
+ (let ((duration (string-trim (buffer-string))))
+ (when (string-match-p "[0-9.]+" duration)
+ (emms-track-set track 'info-playing-time
+ (floor (string-to-number duration))))))))
(provide 'my-emms)
;;; my-emms.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-epub.el b/emacs/.emacs.d/lisp/my/my-epub.el
new file mode 100644
index 0000000..4a3dfca
--- /dev/null
+++ b/emacs/.emacs.d/lisp/my/my-epub.el
@@ -0,0 +1,75 @@
+;;; my-epub.el -- epub utils -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; Package-Requires: ((emacs "30.1"))
+
+;; This file is part of dotted.
+
+;; dotted is free software: you can redistribute it and/or modify it under
+;; the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; dotted is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+;; Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public
+;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; epub utils.
+
+;;; Code:
+
+
+(defun my-epub-content-file-name (file-name)
+ (with-temp-buffer
+ (if (eq 0 (call-process "unzip" nil t nil
+ "-p" file-name "META-INF/container.xml"))
+ (let ((dom (libxml-parse-xml-region (point-min) (point-max))))
+ (dom-attr
+ (dom-by-tag
+ (dom-by-tag (dom-by-tag dom 'container) 'rootfiles)
+ 'rootfile)
+ 'full-path))
+ (message "Failed to extract container.xml: %s" (buffer-string))
+ nil)))
+
+(defun my-epub-metadata (file-name)
+ "Get metadata of an epub file."
+ (when-let ((content-file-name (my-epub-content-file-name file-name)))
+ (with-temp-buffer
+ (call-process "unzip" nil t nil "-p" file-name content-file-name)
+ (let* ((dom (libxml-parse-xml-region (point-min) (point-max)))
+ (metadata (dom-by-tag dom 'metadata))
+ (title (dom-text (dom-by-tag metadata 'title)))
+ (authors (dom-texts (dom-by-tag metadata 'creator) ", "))
+ (identifier
+ (replace-regexp-in-string
+ "[^0-9,]" ""
+ (dom-texts
+ (seq-filter
+ (lambda (node)
+ (or (equal "ISBN" (dom-attr node 'scheme))
+ (string-match-p "^[0-9]+$" (dom-text node))))
+ (dom-by-tag metadata 'identifier))
+ ",")))
+ (date (replace-regexp-in-string
+ "[^0-9]" ""
+ (dom-text (dom-by-tag metadata 'date))))
+ (year (substring date 0 (min 4 (length date)))))
+ `((title . ,title)
+ (authors . ,authors)
+ (year . ,year)
+ (identifier . ,identifier))
+ ;; (pp metadata)
+ ))
+ ))
+
+(provide 'my-epub)
+;;; my-epub.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-gnus.el b/emacs/.emacs.d/lisp/my/my-gnus.el
index 6a2142b..7623548 100644
--- a/emacs/.emacs.d/lisp/my/my-gnus.el
+++ b/emacs/.emacs.d/lisp/my/my-gnus.el
@@ -419,5 +419,28 @@ The archiving target comes from `my-gnus-group-alist'."
(let ((inhibit-message nil))
(message "Copied region with %d links." (length pairs)))))
+(defun my-isync-sync-mail ()
+ "Call `mbsync' to sync mail"
+ (interactive)
+ (message "isync in progress...")
+ (set-process-sentinel
+ (start-process "isync" "*isync*" "mbsync" "-a")
+ (lambda (proc event)
+ (let ((status (process-exit-status proc)))
+ (message "isync in progress...%s: %s"
+ (if (eq status 0) "done" "failed")
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-max))
+ (re-search-backward " ")
+ (buffer-substring (1+ (point)) (point-max))))
+ (gnus-group-get-new-news)))))
+
+(defun my-gnus-group-refresh (arg)
+ "Call `gnus-group-get-new-news' or, with a prefix arg, `my-isync-sync-mail'"
+ (interactive "P")
+ (if arg
+ (my-isync-sync-mail)
+ (gnus-group-get-new-news)))
+
(provide 'my-gnus)
;;; my-gnus.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-libgen.el b/emacs/.emacs.d/lisp/my/my-libgen.el
index c1f430f..a8e7dca 100644
--- a/emacs/.emacs.d/lisp/my/my-libgen.el
+++ b/emacs/.emacs.d/lisp/my/my-libgen.el
@@ -136,14 +136,13 @@
(alist-get 'coverurl info)))))
(defun my-libgen-format-filename (info)
- (replace-regexp-in-string "[:;?/]" "_"
- (format
- "%s - %s (%s) [%s].%s"
- (alist-get 'author info)
- (alist-get 'title info)
- (alist-get 'year info)
- (alist-get 'identifier info)
- (alist-get 'extension info))))
+ (my-make-doc-file-name (format
+ "%s - %s (%s) [%s].%s"
+ (alist-get 'author info)
+ (alist-get 'title info)
+ (alist-get 'year info)
+ (alist-get 'identifier info)
+ (alist-get 'extension info))))
(defvar my-libgen-download-dir "~/Downloads")
@@ -162,13 +161,33 @@
id-head
(downcase (alist-get 'md5 info)))))
+(defun my-libgen-plus-make-download-link-onion (info)
+ (let* ((path
+ (dom-attr
+ (dom-search
+ (my-url-fetch-dom
+ (alist-get 'edition (my-libgen-plus-urls info)))
+ (lambda (n)
+ (string-match "r_[0-9]+_libgen" (or (dom-attr n 'href) ""))))
+ 'href))
+ (id
+ (progn
+ (string-match "r_\\([0-9]+\\)_libgen" path)
+ (match-string 1 path)))
+ (id-head (substring id 0 -3)))
+ (format "%s/LG/%s%s/%s"
+ my-libgen-onion-host
+ (make-string (- 4 (length id-head)) ?0)
+ id-head
+ (downcase (alist-get 'md5 info)))))
+
(defun my-libgen-plus-get-download-url (info)
(let-alist info
(file-name-concat
my-libgen-plus-host
(dom-attr
(dom-search
- (my-url-fetch-dom (format "%s/ads.php?md5=%s" my-libgen-plus-host .md5))
+ (my-wget-dom (alist-get 'ads (my-libgen-plus-urls info)))
(lambda (n)
(string-match (format "get\\.php\\?md5=%s" .md5)
(or (dom-attr n 'href) ""))))
@@ -186,6 +205,48 @@
nil
(lambda () (my-libgen-check-md5 filename md5)))))
+(defun my-libgen-plus-urls (info)
+ (let-alist info
+ `((ads . ,(format "%s/ads.php?md5=%s" my-libgen-plus-host .md5))
+ (edition . ,(format "%s/edition.php?id=%s" my-libgen-plus-host
+ .edition-id))
+ (file . ,(format "%s/file.php?id=%s" my-libgen-plus-host
+ .file-id)))))
+
+(defun my-libgen-plus-print-urls-action ()
+ (interactive)
+ (pp (my-libgen-plus-urls (get-text-property (point) 'button-data))))
+
+(defun my-libgen-plus-download-onion-action ()
+ (interactive)
+ (let* ((info (get-text-property (point) 'button-data))
+ (filename (file-name-concat (expand-file-name my-libgen-download-dir)
+ (my-libgen-format-filename info)))
+ (md5 (alist-get 'md5 info)))
+ (my-wget-async
+ (my-libgen-plus-make-download-link-onion info)
+ filename
+ nil
+ (lambda () (my-libgen-check-md5 filename md5)))))
+
+(defun my-libgen-plus-edition-infobox (info)
+ (let ((dom (my-url-fetch-dom
+ (alist-get 'edition (my-libgen-plus-urls info)))))
+ (infobox-render-string
+ (with-temp-buffer
+ (insert (mapconcat (lambda (p) (dom-texts p ""))
+ (dom-by-tag (dom-by-class dom "order-2") 'p) "\n"))
+ (shr-insert-document (dom-by-class dom "order-5"))
+ (buffer-string))
+ `(my-libgen-plus-edition-infobox ,info)
+ (called-interactively-p 'interactive)
+ )
+ ))
+
+(defun my-libgen-plus-infobox-action ()
+ (interactive)
+ (my-libgen-plus-edition-infobox (get-text-property (point) 'button-data)))
+
(defun my-libgen-check-md5 (file md5)
(let ((actual (substring (my-call-process-out "md5sum" file) 0 32)))
(unless (equal actual md5)
@@ -224,7 +285,9 @@
(let ((kmap (make-sparse-keymap)))
(set-keymap-parent kmap button-map)
(define-key kmap "d" 'my-libgen-plus-download-action)
- ;; (define-key kmap "t" 'my-libgen-download-onion-action)
+ (define-key kmap "i" 'my-libgen-plus-infobox-action)
+ (define-key kmap "t" 'my-libgen-plus-download-onion-action)
+ (define-key kmap "u" 'my-libgen-plus-print-urls-action)
;; (define-key kmap "p" 'my-libgen-show-more-info)
kmap))
@@ -323,6 +386,12 @@
(edition-id . ,edition-id)
(identifier . ,identifier)))))
+(defun my-libgen-plus-guess-md5 (mirrors)
+ (let ((joined
+ (string-join mirrors " ")))
+ (when (string-match "\\<[0-9a-f]\\{32\\}\\>" joined)
+ (match-string 0 joined))))
+
(defun my-libgen-plus-search-parse-tr (tr)
(let* ((tds (dom-by-tag tr 'td))
(title-id (my-libgen-plus-parse-title-id (elt tds 0)))
@@ -343,7 +412,7 @@
(mirrors-td (elt tds 8))
(mirrors (seq-map (lambda (mirror) (dom-attr mirror 'href))
(dom-by-tag mirrors-td 'a)))
- (md5 (when mirrors (substring (car mirrors) 4 36)))
+ (md5 (when mirrors (my-libgen-plus-guess-md5 mirrors)))
)
`((title . ,title)
(identifier . ,identifier)
@@ -439,14 +508,13 @@
(alist-get 'filesize-human info)))
(defun my-libfic-format-filename (info)
- (replace-regexp-in-string "[:;]" "_"
- (format
- "%s - %s (%s) [%s].%s"
- (alist-get 'author info)
- (alist-get 'title info)
- (alist-get 'series info)
- (alist-get 'identifier info)
- (alist-get 'extension info))))
+ (my-make-doc-file-name (format
+ "%s - %s (%s) [%s].%s"
+ (alist-get 'author info)
+ (alist-get 'title info)
+ (alist-get 'series info)
+ (alist-get 'identifier info)
+ (alist-get 'extension info))))
(defun my-grok-libfic-action (info)
(interactive)
diff --git a/emacs/.emacs.d/lisp/my/my-mariadb.el b/emacs/.emacs.d/lisp/my/my-mariadb.el
index d6c2463..1759af2 100644
--- a/emacs/.emacs.d/lisp/my/my-mariadb.el
+++ b/emacs/.emacs.d/lisp/my/my-mariadb.el
@@ -65,7 +65,7 @@
(replace-regexp-in-string
"/src"
"/build/mysql-test/var/log/mysqld.1.1.rr/latest-trace"
- ;; "/build/mysql-test/var/log/mysqld.2.2.rr/latest-trace"
+ ;; "/build/mysql-test/var/log/mysqld.2.1.rr/latest-trace"
(project-root (project-current t))))
(expand-file-name "~/bin/gdb-mi.sh"))))
@@ -291,11 +291,24 @@ switches to the buffer."
(my-save-text-and-switch-to-buffer source file-name)))
(defvar my-mtr-compilation-error-re
- '(mtr "^mysqltest: At line \\([0-9]+\\)" nil 1))
+ '(mtr "^\\([^ ]+\\) +\\(w[0-9]+ \\)?\\[ fail \\]"
+ my-mtr-compilation-error-filename))
-;; (defun my-mtr-find-test-file (test-name &optional dir)
-;; (unless dir (setq dir default-directory))
-;; ())
+(defun my-mtr-compilation-error-filename ()
+ (save-excursion
+ (save-match-data
+ (my-mtr-find-test-file
+ (match-string 1)
+ (project-root (project-current))))))
+
+(defun my-mtr-find-test-file (test-name dir)
+ (pcase-let ((`(,suite ,base) (string-split test-name "\\.")))
+ (seq-find
+ (lambda (file)
+ (string-match-p (format "%s\\(/t\\)?/%s.test$" suite base) file))
+ (directory-files-recursively dir
+ (format "%s.test" base))))
+ )
(defun my-mtr-set-compile-command ()
(when (and buffer-file-name
@@ -306,11 +319,11 @@ switches to the buffer."
(test-name
(progn
(when (string-match
- "^.*/mysql-test/\\(.+?\\)/\\(t/\\)?\\([^/]+\\)\\.test$"
+ "^.*/mysql-test/\\(suite/\\)?\\(.+?\\)/\\(t/\\)?\\([^/]+\\)\\.test$"
buffer-file-name)
(format "%s.%s"
- (match-string 1 buffer-file-name)
- (match-string 3 buffer-file-name))))))
+ (match-string 2 buffer-file-name)
+ (match-string 4 buffer-file-name))))))
(setq-local
compile-command
(format "%s %s %s %s"
@@ -319,5 +332,42 @@ switches to the buffer."
test-name
"--rr")))))
+(defun my-mtr-remove-if-1 ()
+ "Remove if (1) blocks"
+ (interactive)
+ (while (re-search-forward
+ (rx bol (0+ space) "if" (0+ space) "(1)" (0+ space) eol)
+ nil t)
+ (kill-whole-line)
+ (my-delete-pair-dwim)))
+
+(defun my-mtr-remove-if-0 ()
+ "Remove if (0) blocks"
+ (interactive)
+ (while (re-search-forward
+ (rx bol (0+ space) "if" (0+ space) "(0)" (0+ space) eol)
+ nil t)
+ (kill-whole-line)
+ (kill-sexp)))
+
+(defun my-mtr-average ()
+ "Calculate average time of mtr --repeat output."
+ (interactive)
+ (let ((run (make-hash-table :test 'equal))
+ (name) (time))
+ (while (re-search-forward "^\\([^ ]+\\).*pass \\] +\\([0-9]+\\)$" nil t)
+ (setq name (match-string 1)
+ time (string-to-number (match-string 2)))
+ (puthash name (cons time (gethash name run)) run))
+ (with-temp-buffer
+ (maphash
+ (lambda (k v)
+ (insert k " " (format "%d" (/ (seq-reduce '+ v 0) (length v))) "\n"))
+ run)
+ (goto-char (point-min))
+ (sort-lines nil (point-min) (point-max))
+ (align-regexp (point-min) (point-max) "\\(\\s-*\\) ")
+ (message (buffer-string)))))
+
(provide 'my-mariadb)
;;; my-mariadb.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-markup.el b/emacs/.emacs.d/lisp/my/my-markup.el
index 2b1c7f6..2901f13 100644
--- a/emacs/.emacs.d/lisp/my/my-markup.el
+++ b/emacs/.emacs.d/lisp/my/my-markup.el
@@ -64,5 +64,31 @@
(when-let ((text (dom-text (my-xml-get-first-child node tag))))
(replace-regexp-in-string "\n" " " (string-trim text))))
+(defun my-html-render (arg)
+ (interactive "P")
+ (if arg
+ (browse-url-of-buffer)
+ (let ((show-trailing-whitespace nil))
+ (call-interactively 'shr-render-buffer)
+ (view-mode))))
+
+(defvar-keymap htmlv-mode-map
+ "." #'htmlv-reopen-as-html
+ )
+
+(define-derived-mode htmlv-mode special-mode "HTML View"
+ "Major mode for viewing HTML documents."
+ (let ((inhibit-read-only t))
+ (shr-render-region (point-min) (point-max)))
+ (set-buffer-modified-p nil)
+ (goto-char (point-min)))
+
+(defun htmlv-reopen-as-html ()
+ (interactive)
+ (with-current-buffer
+ (cl-letf (((symbol-function 'y-or-n-p) #'always))
+ (find-file-literally buffer-file-name))
+ (mhtml-mode)))
+
(provide 'my-markup)
;;; my-markup.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-media-segment.el b/emacs/.emacs.d/lisp/my/my-media-segment.el
index f222316..e8ee5cc 100644
--- a/emacs/.emacs.d/lisp/my/my-media-segment.el
+++ b/emacs/.emacs.d/lisp/my/my-media-segment.el
@@ -50,18 +50,93 @@ The process can be started by applying 'start-process' on START-PROCESS-ARGS."
(when my-media-segment-queued-jobs
(funcall (pop my-media-segment-queued-jobs))))
-(defun my-segment-media-file-1 (media-file-name desc-file-name)
+(defun my-ffmpeg-split-file (file-name split-at)
+ "Split FILE-NAME at SPLIT-AT into two files."
+ (let* ((name-no-ext (file-name-sans-extension file-name))
+ (ext (file-name-extension file-name))
+ (file-name-1 (make-temp-file (format "%s-1-" name-no-ext) nil
+ (format ".%s" ext)))
+ (file-name-2 (make-temp-file (format "%s-2-" name-no-ext) nil
+ (format ".%s" ext))))
+ (message "Splitting %s at %s into %s and %s..."
+ file-name split-at file-name-1 file-name-2)
+ (set-process-sentinel
+ (start-process (format "ffmpeg-%s" file-name)
+ (format "*ffmpeg-%s*" file-name)
+ "ffmpeg"
+ "-i" file-name
+ "-to" split-at "-c" "copy" file-name-1
+ "-ss" split-at "-c" "copy" file-name-2
+ "-y")
+ (lambda (proc event)
+ (let ((status (process-exit-status proc)))
+ (if (eq status 0)
+ (progn
+ (message "Splitting %s at %s into %s and %s... Done"
+ file-name split-at file-name-1 file-name-2))
+ (message "Splitting %s at %s into %s and %s... Failed: %s"
+ file-name split-at file-name-1 file-name-2 event)))))))
+
+(defun my-dired-do-ffmpeg-split-file ()
+ (interactive)
+ (seq-do
+ (lambda (file)
+ (my-ffmpeg-split-file file (read-string
+ (format "Split %s at: " file))))
+ (dired-get-marked-files)))
+
+(defun my-segment-media-file-2 (media-file-name info-file-name)
+ "Run ffmpeg to segment MEDIA-FILE-NAME according to INFO-FILE-NAME in one go.
+
+Much faster than my-segment-media-file or my-segment-media-file-1."
+ (interactive (list
+ (read-file-name "Choose media file: ")
+ (read-file-name
+ "Choose description file (.info.json or .description): ")))
+ (let* ((dir (file-name-sans-extension (expand-file-name media-file-name)))
+ (info (my-get-media-segments info-file-name))
+ (total (length info))
+ (pad (1+ (floor (log10 total))))
+ (idx 0)
+ (args `("-i" ,(expand-file-name media-file-name))))
+ (ignore-errors (dired-create-directory dir))
+ (dolist (media info)
+ (setq idx (1+ idx))
+ (let* ((title (plist-get media :title))
+ (start (plist-get media :start))
+ (end (plist-get media :end)))
+ (setq args (append args
+ `("-ss" ,start)
+ (when end `("-to" ,end))
+ `("-c" "copy"
+ ,(format
+ (format "%%s/%%0%dd-%%s.%%s" pad) dir idx title
+ (file-name-extension media-file-name)))))
+ (message "Will cut %s-%s to %s (%d/%d)..."
+ start (or end "") title idx total)))
+ (set-process-sentinel
+ (apply 'start-process
+ (append `(,(format "ffmpeg-%s" media-file-name)
+ ,(format "*ffmpeg-%s*" media-file-name)
+ "ffmpeg")
+ args))
+ (lambda (proc event)
+ (let ((status (process-exit-status proc)))
+ (if (eq status 0)
+ (progn
+ (message "Cutting %s: All DONE" media-file-name))
+ (message "Cutting %s FAILED: %s" media-file-name event)))))))
+
+(defun my-segment-media-file-1 (media-file-name info-file-name)
"Run ffmpeg asynchronously to segment file-name according to description.
Uses `my-media-segment-max-inflight' to limit number of inflight tasks."
(interactive (list
(read-file-name "Choose media file: ")
- (read-file-name "Choose description file: ")))
+ (read-file-name
+ "Choose description file (.info.json or .description): ")))
(let* ((dir (file-name-sans-extension (expand-file-name media-file-name)))
- (info (my-get-media-segments
- (with-temp-buffer
- (insert-file-contents desc-file-name)
- (buffer-string))))
+ (info (my-get-media-segments info-file-name))
(total (length info))
(pad (1+ (floor (log10 total))))
(idx 0)
@@ -94,12 +169,31 @@ Uses `my-media-segment-max-inflight' to limit number of inflight tasks."
(funcall thunk)
(my-media-segment-enqueue-process thunk))))))
-(defun my-get-media-segments (description)
+(defun my-get-media-segments (info-file-name)
+ (if (equal (file-name-extension info-file-name) "json")
+ (my-get-media-segments-from-json info-file-name)
+ (my-get-media-segments-from-descr info-file-name)))
+
+(defun my-get-media-segments-from-json (json-file-name)
+ (let ((info
+ (with-temp-buffer
+ (insert-file-contents json-file-name)
+ (goto-char (point-min))
+ (json-read))))
+ (seq-map
+ (lambda (ch)
+ (let-alist ch
+ ;; .title: ytdl; .tags.titile: .m4b
+ (list :title (my-make-doc-file-name (or .title .tags.title))
+ :start (format "%s" .start_time)
+ :end (format "%s" .end_time))))
+ (alist-get 'chapters info))))
+
+(defun my-get-media-segments-from-descr (descr-file-name)
"Output title start end triplets."
(let ((results) (title) (start) (end))
(with-temp-buffer
- (erase-buffer)
- (insert description)
+ (insert-file-contents descr-file-name)
(goto-char (point-min))
(save-excursion
(while (re-search-forward
@@ -116,7 +210,7 @@ Uses `my-media-segment-max-inflight' to limit number of inflight tasks."
(buffer-substring-no-properties
(point)
(progn (beginning-of-line 2) (point))))))
- (push (list :title (my-make-filename title) :start start :end end) results)
+ (push (list :title (my-make-doc-file-name title) :start start :end end) results)
)
(setq end nil)
(dolist (result results)
@@ -127,19 +221,16 @@ Uses `my-media-segment-max-inflight' to limit number of inflight tasks."
)))
(defvar my-segment-media-max-async 10)
-(defun my-segment-media-file (media-file-name desc-file-name synchronously)
+(defun my-segment-media-file (media-file-name info-file-name synchronously)
"Run ffmpeg asynchronously to segment file-name according to description.
With a prefix-arg, run synchronously."
(interactive (list
(read-file-name "Choose media file: ")
- (read-file-name "Choose description file: ")
+ (read-file-name "Choose info file: ")
current-prefix-arg))
(let* ((dir (file-name-sans-extension (expand-file-name media-file-name)))
- (info (my-get-media-segments
- (with-temp-buffer
- (insert-file-contents desc-file-name)
- (buffer-string))))
+ (info (my-get-media-segments info-file-name))
(total (length info))
(idx 0))
(when (or synchronously (<= total my-segment-media-max-async)
diff --git a/emacs/.emacs.d/lisp/my/my-net.el b/emacs/.emacs.d/lisp/my/my-net.el
index 6212b50..a608808 100644
--- a/emacs/.emacs.d/lisp/my/my-net.el
+++ b/emacs/.emacs.d/lisp/my/my-net.el
@@ -29,6 +29,7 @@
;;; net utilities
(defvar my-download-dir "~/Downloads")
+(defvar my-webpage-incoming-dir "~/Downloads")
(defmacro my-url-as-googlebot (&rest body)
"Run BODY while spoofing as googlebot"
diff --git a/emacs/.emacs.d/lisp/my/my-nov.el b/emacs/.emacs.d/lisp/my/my-nov.el
index 9a819c7..21df675 100644
--- a/emacs/.emacs.d/lisp/my/my-nov.el
+++ b/emacs/.emacs.d/lisp/my/my-nov.el
@@ -28,22 +28,51 @@
(require 'nov)
+(defvar my-nov-mode-line-format "%p%% %t: %c")
+(defvar-local my-nov-title nil)
+(defvar-local my-nov-chapter-title nil)
+(defvar-local my-nov-position-percent nil)
+
;; override nov-render-title
;; this is because header line does not work with follow mode
(defun my-nov-render-title (dom)
"Custom <title> rendering function for DOM.
Sets `header-line-format' to a combination of the EPUB title and
chapter title."
- (let ((title (cdr (assq 'title nov-metadata)))
- (chapter-title (car (esxml-node-children dom))))
- (when (not chapter-title)
- (setq chapter-title "No title"))
- ;; this shouldn't happen for properly authored EPUBs
- (when (not title)
- (setq title "No title"))
+ (setq my-nov-title (cdr (assq 'title nov-metadata))
+ my-nov-chapter-title (car (esxml-node-children dom))))
+
+(defun my-nov-update-mode-line ()
+ (setq my-nov-position-percent
+ (/ (* 100 (my-nov-word-position)) my-nov-total-word-count))
+ (let ((title (or my-nov-title (propertize "No title" 'face 'italic)))
+ (chapter-title (or my-nov-chapter-title
+ (propertize "No title" 'face 'italic))))
(setq mode-line-buffer-identification
- (concat title ": " chapter-title))
- ))
+ (format-spec
+ my-nov-mode-line-format
+ `((?c . ,chapter-title)
+ (?t . ,title)
+ (?p . ,my-nov-position-percent))))))
+
+(defun my-nov-render-span (dom)
+ (unless (equal (dom-attr dom 'epub:type) "pagebreak")
+ (shr-generic dom)))
+
+;;; TODO: perhaps no indentation?
+(defun my-nov-render-ol (dom)
+ (shr-ensure-paragraph)
+ (let* ((attrs (dom-attributes dom))
+ (start-attr (alist-get 'start attrs))
+ ;; Start at 1 if there is no start attribute
+ ;; or if start can't be parsed as an integer.
+ (start-index (condition-case _
+ (cl-parse-integer start-attr)
+ (t nil)))
+ (shr-list-mode (or start-index 'ul))
+ (shr-internal-bullet `(" " . ,(shr-string-pixel-width " "))))
+ (shr-generic dom))
+ (shr-ensure-paragraph))
(defun my-nov-find-file-with-ipath (file-name ipath)
"Find epub file and goto IPATH.
@@ -84,5 +113,98 @@ Useful for recoll."
(visual-line-mode)
)
+(defvar-local my-nov-document-word-counts nil
+ "Word count of each nov document.")
+
+(defvar-local my-nov-total-word-count nil
+ "Total word count of the epub.")
+
+(defun my-nov-count-words ()
+ (interactive)
+ (unless my-nov-document-word-counts
+ (message "Counting words...")
+ (setq my-nov-document-word-counts
+ (apply
+ 'vector
+ (seq-map
+ (lambda (doc)
+ (with-temp-buffer
+ (pcase-let ((`(,name . ,file) doc))
+ (insert-file-contents file)
+ (nov-render-html)
+ (cons name (count-words (point-min) (point-max))))))
+ nov-documents)))
+ (setq my-nov-total-word-count
+ (seq-reduce
+ (lambda (sum pair)
+ (+ sum (cdr pair)))
+ my-nov-document-word-counts
+ 0))
+ (message "Counting words...done")))
+
+(defun my-nov-stats ()
+ (interactive)
+ (message "%d words; %d standard pages"
+ my-nov-total-word-count
+ (ceiling (/ my-nov-total-word-count 300.0))))
+
+;;; TODO: also show current percentage in the total book in the mode
+;;; line
+(defun my-nov-goto-nth-word (n)
+ "Go to the nth word of the current epub."
+ (my-nov-count-words)
+ (setq nov-documents-index -1)
+ (let ((found
+ (seq-find
+ (lambda (pair)
+ (setq n (- n (cdr pair)))
+ (setq nov-documents-index (1+ nov-documents-index))
+ (<= n 0))
+ my-nov-document-word-counts)))
+ (nov-render-document)
+ (if (> n 0)
+ (end-of-buffer)
+ (forward-word (+ n (cdr found)))))
+ )
+
+(defun my-nov-word-position ()
+ "Where are we in terms of word position?
+
+Return n, such that nth word of the epub is at point."
+ (my-nov-count-words)
+ (let ((result 0))
+ (dotimes (i nov-documents-index)
+ (setq result (+ result (cdr (aref my-nov-document-word-counts i)))))
+ (setq result (+ result (count-words (point-min) (point))))))
+
+(defun my-nov-skim-forward ()
+ "Forward by 3-10% of the book."
+ (interactive)
+ (let ((pc (+ 3 (random 8))))
+ (my-nov-goto-nth-word
+ (+ (my-nov-word-position)
+ (/ (* my-nov-total-word-count pc) 100)))
+ (message "Skimmed forward by %d%% of the book" pc)))
+
+(defun my-nov-skim-backward ()
+ "Backward by 3-10% of the book."
+ (interactive)
+ (let ((pc (+ 3 (random 8))))
+ (my-nov-goto-nth-word
+ (max
+ 0
+ (- (my-nov-word-position)
+ (/ (* my-nov-total-word-count pc) 100))))
+ (message "Skimmed backward by %d%% of the book" pc)))
+
+(defun my-nov-goto-random-position ()
+ "Goto a random position in the epub."
+ (interactive)
+ (my-nov-count-words)
+ (let ((n (random my-nov-total-word-count)))
+ (my-nov-goto-nth-word n)
+ (message "Went to the %dth word (%d%% of the book)."
+ n (/ (* n 100) my-nov-total-word-count))))
+
(provide 'my-nov)
;;; my-nov.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-org-remark.el b/emacs/.emacs.d/lisp/my/my-org-remark.el
index 3e0ef0a..4582f6c 100644
--- a/emacs/.emacs.d/lisp/my/my-org-remark.el
+++ b/emacs/.emacs.d/lisp/my/my-org-remark.el
@@ -26,6 +26,71 @@
;;; Code:
+
+;;; override `org-remark-highlight-add-or-update-highlight-headline'
+(defun my-org-remark-highlight-add-or-update-highlight-headline (highlight source-buf notes-buf)
+ "Add a new HIGHLIGHT headlne to the NOTES-BUF or update it.
+Return notes-props as a property list.
+
+HIGHLIGHT is an overlay from the SOURCE-BUF.
+
+Assume the current buffer is NOTES-BUF and point is placed on the
+beginning of source-headline, which should be one level up."
+ ;; Add org-remark-link with updated line-num as a property
+ (let (title beg end props id text filename link orgid org-remark-type other-props)
+ (with-current-buffer source-buf
+ (setq title (org-remark-highlight-get-title)
+ beg (overlay-start highlight)
+ end (overlay-end highlight)
+ props (overlay-properties highlight)
+ id (plist-get props 'org-remark-id)
+ org-remark-type (overlay-get highlight 'org-remark-type)
+ text (org-with-wide-buffer
+ (org-remark-highlight-headline-text highlight org-remark-type))
+ filename (org-remark-source-get-file-name
+ (org-remark-source-find-file-name))
+ link (run-hook-with-args-until-success
+ 'org-remark-highlight-link-to-source-functions filename beg)
+ orgid (org-remark-highlight-get-org-id beg)
+ other-props (org-remark-highlight-collect-other-props highlight))
+ ;; TODO ugly to add the beg end after setq above
+ (plist-put props org-remark-prop-source-beg (number-to-string beg))
+ (plist-put props org-remark-prop-source-end (number-to-string end))
+ (when link (plist-put props "org-remark-link" link))
+ (when other-props (setq props (append props other-props))))
+ ;;; Make it explicit that we are now in the notes-buf, though it is
+ ;;; functionally redundant.
+ (with-current-buffer notes-buf
+ (let ((highlight-headline (org-find-property org-remark-prop-id id))
+ ;; Assume point is at the beginning of the parent headline
+ (level (1+ (org-current-level))))
+ (if highlight-headline
+ (progn
+ (goto-char highlight-headline)
+ ;; Update the existing headline and position properties
+ ;; Don't update the headline text when it already exists.
+ ;; Let the user decide how to manage the headlines
+ ;; (org-edit-headline text)
+ (org-remark-notes-set-properties props))
+ ;; No headline with the marginal notes ID property. Create a new one
+ ;; at the end of the file's entry
+ (org-narrow-to-subtree)
+ (goto-char (point-max))
+ ;; Ensure to be in the beginning of line to add a new headline
+ (when (eolp) (open-line 1) (forward-line 1) (beginning-of-line))
+ ;; Create a headline
+ ;; Add a properties
+ (insert (concat (insert-char (string-to-char "*") level)
+ " " (my-elide-text text fill-column) "\n"))
+ ;; org-remark-original-text should be added only when this
+ ;; headline is created. No update afterwards
+ (plist-put props "org-remark-original-text" text)
+ (org-remark-notes-set-properties props)
+ (when (and orgid org-remark-use-org-id)
+ (insert (concat "[[id:" orgid "]" "[" title "]]"))))
+ (list :body (org-remark-notes-get-body)
+ :original-text text)))))
+
(defun my-org-remark-open-or-create ()
(interactive)
(if mark-active
diff --git a/emacs/.emacs.d/lisp/my/my-org.el b/emacs/.emacs.d/lisp/my/my-org.el
index f870d4f..5a50673 100644
--- a/emacs/.emacs.d/lisp/my/my-org.el
+++ b/emacs/.emacs.d/lisp/my/my-org.el
@@ -28,6 +28,7 @@
(require 'org)
+(require 'tor)
;;; org mode
(defun my-org-open-shell-at-attach-dir ()
@@ -179,8 +180,8 @@ notes file."
(with-current-buffer (find-file-noselect org-default-notes-file)
(clone-indirect-buffer nil t)
(setq my-notes-buffer-list
- (setq-filter 'my-buffer-with-same-base-p
- (buffer-list))))
+ (seq-filter 'my-buffer-with-same-base-p
+ (buffer-list))))
(if (eq last-command 'my-org-open-or-cycle-notes)
(progn
(setq my-notes-buffer-list
@@ -1662,5 +1663,28 @@ dual relation link-back on that task."
(and (org-entry-get (point) "BLOCKED_BY")
(member (org-entry-get nil "TODO") org-not-done-keywords)))
+(defun my-org-clock-split ()
+ "Split the clock entry at the current line."
+ (interactive)
+ (let ((line (buffer-substring (line-beginning-position) (line-end-position))))
+ (unless (string-match org-element-clock-line-re line)
+ (error "Not at an org clock line"))
+ (let* ((start (match-string 1 line))
+ (end (match-string 2 line))
+ (mid (org-read-date t 'to-time nil "Split org clock at: " nil start)))
+ (back-to-indentation)
+ (kill-line)
+ (insert "CLOCK: [" start "]--")
+ (org-insert-time-stamp mid t t)
+ (org-clock-update-time-maybe)
+
+ (my-new-line-above-or-below)
+ (insert "CLOCK: ")
+ (org-insert-time-stamp mid t t)
+ (insert "--[" end "]")
+ (org-clock-update-time-maybe)
+ ))
+ )
+
(provide 'my-org)
;;; my-org.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-prog.el b/emacs/.emacs.d/lisp/my/my-prog.el
index 92fcf21..eead408 100644
--- a/emacs/.emacs.d/lisp/my/my-prog.el
+++ b/emacs/.emacs.d/lisp/my/my-prog.el
@@ -444,7 +444,12 @@ overlay arrow in source buffer."
(defun my-gud-watch-expr (expr)
(with-current-buffer gud-comint-buffer
- (insert "watch -l " expr)
+ (insert "wl " expr)
+ (comint-send-input)))
+
+(defun my-gud-break-expr (expr)
+ (with-current-buffer gud-comint-buffer
+ (insert "b " expr)
(comint-send-input)))
(defun my-gud-print-expr (expr)
@@ -454,11 +459,12 @@ overlay arrow in source buffer."
(defun my-gud-print-expr-region (b e)
(interactive "r")
- (unless (eq (gdb-get-source-buffer) (current-buffer))
- (error "Not in the source buffer"))
- (if current-prefix-arg
- (my-gud-watch-expr (buffer-substring b e))
- (my-gud-print-expr (buffer-substring b e))))
+ (let ((selection (buffer-substring b e)))
+ (pcase (prefix-numeric-value current-prefix-arg)
+ (16 (my-gud-break-expr selection))
+ (4 (my-gud-watch-expr selection))
+ (_ (my-gud-print-expr selection))))
+ (deactivate-mark))
;;; which-func
(defun my-copy-which-func ()
@@ -466,11 +472,19 @@ overlay arrow in source buffer."
(kill-new (which-function))
)
+(defun my-copy-with-func (b e)
+ (interactive "r")
+ (kill-new
+ (concat comment-start "in " (which-function) ":" comment-end "\n"
+ (buffer-substring b e)))
+ (deactivate-mark)
+ (message "Copied current region with function name"))
+
(defun my-set-header-line-to-which-func ()
(setq header-line-format
- '((which-func-mode
- ("" which-func-format " ")
- ))))
+ '((which-func-mode
+ ("" which-func-format " ")
+ ))))
;; override bookmark-make-record for easier default bookmark name.
(defun my-bookmark-make-record ()
diff --git a/emacs/.emacs.d/lisp/my/my-ttrss.el b/emacs/.emacs.d/lisp/my/my-ttrss.el
new file mode 100644
index 0000000..046f596
--- /dev/null
+++ b/emacs/.emacs.d/lisp/my/my-ttrss.el
@@ -0,0 +1,200 @@
+;;; my-ttrss.el -- ttrss utilities -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; Package-Requires: ((emacs "30.1"))
+
+;; This file is part of dotted.
+
+;; dotted is free software: you can redistribute it and/or modify it under
+;; the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; dotted is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+;; Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public
+;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; ttrss utilities.
+
+;;; Code:
+
+(require 'ttrss)
+(require 'my-utils)
+(require 'org-macs)
+
+;;; TODO: my-ttrss-save-recent
+
+(defun my-ttrss-hello ()
+ (let ((sid (ttrss-login ttrss-address ttrss-user ttrss-password)))
+ (message "Server running version %s and API level %d\n"
+ (ttrss-get-version ttrss-address sid)
+ (ttrss-get-api-level ttrss-address sid))
+ (message "There are %s unread articles in %d feeds"
+ (ttrss-get-unread ttrss-address sid)
+ (length (ttrss-get-feeds ttrss-address sid :unread_only t)))))
+
+(defun my-ttrss-fetch-feeds ()
+ (let ((sid (ttrss-login ttrss-address ttrss-user ttrss-password)))
+ (ttrss-get-feeds ttrss-address sid :cat_id -3)))
+
+(defun my-ttrss-feed-dir (feed-title feed-id)
+ (file-name-concat
+ my-ttrss-dir
+ (my-make-doc-file-name
+ (format "%s [ttrss%s]" feed-title feed-id))))
+
+(defun my-ttrss-feed-last-id-file (feed-info)
+ (let* ((title (plist-get feed-info :title))
+ (id (plist-get feed-info :id)))
+ (expand-file-name
+ (file-name-concat (my-ttrss-feed-dir title id) "last-id"))))
+
+(defun my-ttrss-feed-get-last-id (feed-info)
+ (let* ((file (my-ttrss-feed-last-id-file feed-info)))
+ (if (file-exists-p file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (string-to-number (buffer-string)))
+ 0)))
+
+(defun my-ttrss-feed-write-last-id (feed-info last-id)
+ (let* ((file (my-ttrss-feed-last-id-file feed-info))
+ (inhibit-message t))
+ (with-temp-buffer
+ (insert (format "%d" last-id))
+ (write-file file))))
+
+(defun my-ttrss-fetch ()
+ "Fetch and save latest articles from all feeds."
+ (interactive)
+ (let* ((sid (ttrss-login ttrss-address ttrss-user ttrss-password))
+ (feeds (ttrss-get-feeds ttrss-address sid :cat_id -3))
+ (n (length feeds)))
+ (seq-do-indexed
+ (lambda (feed i)
+ (my-ttrss-fetch-feed-articles
+ sid feed
+ (format "[my-ttrss] (%d/%d) Fetching articles from %s..."
+ (1+ i) n (plist-get feed :title))))
+ feeds)))
+
+(defun my-ttrss-fetch-feed-articles (sid feed &optional message-head)
+ (unless message-head
+ (setq message-head
+ (format "[my-ttrss] Fetching articles from %s..."
+ (plist-get feed :title))))
+ (message "%s" message-head)
+ (let ((articles
+ (ttrss-get-headlines
+ ttrss-address sid
+ :feed_id (plist-get feed :id) :show_content t :include_attachments t
+ :since_id (my-ttrss-feed-get-last-id feed))))
+ (seq-do 'my-ttrss-save-article articles)
+ (unless (seq-empty-p articles)
+ (my-ttrss-feed-write-last-id
+ feed
+ (seq-reduce
+ (lambda (acc article) (max acc (plist-get article :id)))
+ articles
+ 0)))
+ (message
+ "%s Done - total %d articles"
+ message-head (length articles))))
+
+(defun my-ttrss-fetch-latest-articles (n &optional since-id)
+ "Fetch N latest articles."
+ (let ((sid (ttrss-login ttrss-address ttrss-user ttrss-password)))
+ (unless since-id (setq since-id 0))
+ (if (ttrss-logged-in-p ttrss-address sid)
+ (ttrss-get-headlines
+ ttrss-address sid
+ :feed_id -4 :limit n :show_content t :include_attachments t
+ :since_id since-id)
+ (message "Login failed"))))
+
+(defun my-ttrss-save-article (info)
+ (with-temp-buffer
+ (insert "<!--\n Page saved with my-ttrss on "
+ (current-time-string) "\n"
+ (json-serialize (org-plist-delete info :content))
+ "\n-->\n")
+ (insert "<h2>" "<a href=\"" (plist-get info :link) "\">"
+ (plist-get info :title) "</a>" "</h2>")
+ (insert "<p>" "<a href=\"" (plist-get info :site_url) "\">"
+ (plist-get info :feed_title) "</a>")
+ (when-let ((author (plist-get info :author)))
+ (unless (or (string-empty-p author)
+ (equal author (plist-get info :feed_title)))
+ (insert " (" author ")")))
+ (let ((updated (format-time-string
+ "%Y-%m-%d %a %H:%M:%S"
+ (encode-time (decode-time (plist-get info :updated))))))
+ (insert " " updated)
+ (insert "</p>")
+ (let ((tags (plist-get info :tags)))
+ (unless (seq-empty-p tags)
+ (insert "<p>tags: " (string-join tags ";") "</p>")))
+ (insert (plist-get info :content))
+ (let ((attached (plist-get info :attachments)))
+ (unless (seq-empty-p attached)
+ (insert "<p>Article attachments:</p>\n<ul>")
+ (seq-do (lambda (at)
+ (let ((title (plist-get at :title))
+ (url (plist-get at :content_url)))
+ (insert "\n<li><a href=" url ">"
+ (if (string-empty-p title) url title)
+ "</a></li>")))
+ attached)
+ (insert "\n</ul>")))
+ (let* ((change-major-mode-with-file-name nil)
+ (coding-system-for-write 'utf-8)
+ (inhibit-message t)
+ (file-name (my-ttrss-format-file-name info))
+ (dir (file-name-directory file-name)))
+ (unless (file-exists-p dir) (make-directory dir t))
+ (write-file file-name)
+ (my-touch-file-mtime file-name updated)))))
+
+(defvar my-ttrss-dir "~/Downloads")
+
+(defun my-ttrss-format-file-name (info)
+ "Format: $author - $title ($year) [ttrss$id].html"
+ (let* ((author (plist-get info :author))
+ (feed-title (plist-get info :feed_title))
+ (feed-id (plist-get info :feed_id))
+ (name
+ (expand-file-name
+ (file-name-concat
+ (my-ttrss-feed-dir feed-title feed-id)
+ (my-make-doc-file-name
+ (format "%s - %s (%s) [ttrss%s].html"
+ (if (string-empty-p author)
+ feed-title
+ author)
+ (plist-get info :title)
+ (format-time-string
+ "%Y"
+ (encode-time (decode-time (plist-get info :updated))))
+ (plist-get info :id)))))))
+ (if (length> name 250)
+ (expand-file-name
+ (file-name-concat
+ (my-ttrss-feed-dir feed-title feed-id)
+ (my-make-doc-file-name
+ (format "_ - _ (%s) [ttrss%s].html"
+ (format-time-string
+ "%Y"
+ (encode-time (decode-time (plist-get info :updated))))
+ (plist-get info :id)))))
+ name)))
+
+(provide 'my-ttrss)
+;;; my-ttrss.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-utils.el b/emacs/.emacs.d/lisp/my/my-utils.el
index 0743227..05ca2e6 100644
--- a/emacs/.emacs.d/lisp/my/my-utils.el
+++ b/emacs/.emacs.d/lisp/my/my-utils.el
@@ -226,6 +226,9 @@ Example: (format-time-string ... (my-time-from-epoch 1698582504))"
(replace-regexp-in-string "[[:punct:][:space:]\n\r]+" sep
(string-trim name)))
+(defun my-make-doc-file-name (name)
+ (replace-regexp-in-string "[:;?/]" "_" name))
+
(defun my-make-filename-from-url (url)
(let* ((urlobj (url-generic-parse-url url))
(filename (url-filename urlobj))
@@ -295,6 +298,7 @@ Example: (format-time-string ... (my-time-from-epoch 1698582504))"
(defvar my-video-incoming-dir my-incoming-dir)
(defvar my-audio-incoming-dir my-incoming-dir)
(defvar my-document-incoming-dir my-incoming-dir)
+(defvar my-music-incoming-dir my-incoming-dir)
(defmacro my-with-default-directory (dir &rest body)
"Run BODY with the default directory."
@@ -311,21 +315,13 @@ Example: (format-time-string ... (my-time-from-epoch 1698582504))"
(apply 'call-process (append (list command nil t nil) args))
(buffer-string)))
-(defun my-call-process-with-torsocks
- (program &optional infile destination display &rest args)
- (apply 'call-process
- (append (list "torsocks" infile destination display program) args)))
-
-(defun my-start-process-with-torsocks (no-tor name buffer program &rest program-args)
- (if no-tor
- (apply 'start-process (append (list name buffer program) program-args))
- (apply 'start-process
- (append (list name buffer "torsocks" program) program-args))))
-
(defun my-touch-new-file (filename)
"Touch a new file."
(with-temp-buffer (write-file filename)))
+(defun my-touch-file-mtime (file date)
+ (call-process "touch" nil nil nil "-d" date file))
+
(defvar my-extension-types
'((audio . ("asf" "cue" "flac" "m4a" "m4r" "mid" "mp3" "ogg" "opus"
"wav" "wma" "spc" "mp4"))
diff --git a/emacs/.emacs.d/lisp/my/my-web.el b/emacs/.emacs.d/lisp/my/my-web.el
index d1eacb6..87c319f 100644
--- a/emacs/.emacs.d/lisp/my/my-web.el
+++ b/emacs/.emacs.d/lisp/my/my-web.el
@@ -59,14 +59,15 @@
(interactive)
(let ((url (plist-get eww-data :url)))
(when (and (string-match "^\\(.*//.*?/\\).*$" url)
- (match-string 1 url))
+ (match-string 1 url))
(eww (match-string 1 url)))))
+(defvar my-tor-browser-bin "tor-browser")
+
(defun my-browse-url-tor-browser (url)
"Browse URL with tor-browser."
(setq url (browse-url-encode-url url))
- (start-process (concat "tor-browser " url) nil "tor-browser"
- "--allow-remote" url))
+ (start-process "tor-browser" nil my-tor-browser-bin "--allow-remote" url))
(defun my-browse-url-firefox-private (url)
"Browse URL in a private firefox window."
@@ -146,10 +147,10 @@ Useful for bypassing \"Enable JavaScript and cookies to continue\"."
(if no-overwrite
(my-make-unique-file-name
(my-make-file-name-from-url url)
- my-download-dir)
+ my-webpage-incoming-dir)
(expand-file-name
(my-make-file-name-from-url url "html")
- my-download-dir))))
+ my-webpage-incoming-dir))))
(url-copy-file url file-name (not no-overwrite))
(browse-url-firefox (format "file://%s" file-name))))
@@ -252,5 +253,71 @@ https://emacs.stackexchange.com/questions/40887/in-org-mode-how-do-i-link-to-int
(lambda (_)
(funcall my-url-context-function url)))))
+(defvar my-dw-host "dw.com")
+
+(defun my-dw-parse-article-url (url)
+ "Returns (lang . article-id)"
+ (let* ((urlobj (url-generic-parse-url url))
+ (path (url-filename urlobj))
+ (components (string-split path "/")))
+ `(,(elt components 1) . ,(string-remove-prefix "a-" (elt components 3)))))
+
+(defun my-dw-article-api (url)
+ (pcase-let ((`(,lang . ,id) (my-dw-parse-article-url url)))
+ (my-url-fetch-json
+ (format "https://%s/graph-api/%s/content/article/%s" my-dw-host lang id))))
+
+(defun my-dw-extract (info)
+ "Returns list of (url . file-name) pairs."
+ (let* ((content (alist-get 'content (alist-get 'data info)))
+ (dir (file-name-concat my-audio-incoming-dir
+ (my-make-doc-file-name
+ (alist-get 'title content))))
+ (audios (alist-get 'audios content)))
+ (seq-map
+ (lambda (audio)
+ (let ((url (alist-get 'mp3Src audio)))
+ `(,url
+ .
+ ,(expand-file-name
+ (file-name-concat dir (file-name-with-extension
+ (my-make-doc-file-name
+ (alist-get 'name audio))
+ (file-name-extension url)))))))
+ audios)))
+
+(defun my-dw-download (pairs)
+ "Download list of (url . file-name) pairs with aria2."
+ (let ((file (make-temp-file "/tmp/aria2"))
+ (n (length pairs)))
+ (with-temp-file file
+ (dolist (pair pairs)
+ (insert (car pair) "\n out=" (cdr pair) "\n"))
+ ;; (buffer-string)
+ )
+ (message "Downloading %d files..." n)
+ (set-process-sentinel
+ (start-process "aria2" "*aria2*" "aria2c" "-x" "5" "-d" "/"
+ "-R" "true" "-i" file)
+ (lambda (proc event)
+ (let ((status (process-exit-status proc)))
+ (if (eq status 0)
+ (progn
+ (message "Downloading %d files...Done" n))
+ (message "Downloading %d files...Failed: %s" n event)))))))
+
+(defun my-dw-download-url (url)
+ (interactive "sDW Download URL: ")
+ (my-dw-download (my-dw-extract (my-dw-article-api url))))
+
+(defun my-dw-download-urls (urls)
+ (my-dw-download (seq-mapcat
+ (lambda (url) (my-dw-extract (my-dw-article-api url)))
+ urls)))
+
+(defun my-local-archive-open-url (url)
+ "Open url from local archive."
+ )
+
(provide 'my-web)
;;; my-web.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-wget.el b/emacs/.emacs.d/lisp/my/my-wget.el
index e7283aa..f3f6771 100644
--- a/emacs/.emacs.d/lisp/my/my-wget.el
+++ b/emacs/.emacs.d/lisp/my/my-wget.el
@@ -30,6 +30,7 @@
;; wget
(require 'wget)
(require 'my-utils)
+(require 'tor)
(defvar my-wget-video-archive-directory)
;; FIXME: this list is rather random...
(setq my-wget-video-extensions '("mp4" "flv" "mkv" "webm" "ogv" "avi"
@@ -53,8 +54,7 @@
(my-start-process-with-torsocks
no-tor "wget" "*wget*" "wget" url "-c" "-O" filename)
(lambda (proc event)
- (let ((ps (process-status proc))
- (status (process-exit-status proc)))
+ (let ((status (process-exit-status proc)))
(if (eq status 0)
(progn
(message "[DONE] Fetched %s to %s" url filename)
@@ -86,5 +86,22 @@
no-tor move-if-video-or-large)
(setq i (1+ i)))))
+(defun my-wget-out-internal (url buffer-processor &optional no-tor)
+ "Run wget on url, dump the results in a temp buffer, then apply BUFFER-PROCESSOR"
+ (with-temp-buffer
+ (my-call-process-with-torsocks no-tor "wget" nil '(t nil) nil "-O" "-" url)
+ (call-interactively 'delete-trailing-whitespace)
+ (funcall buffer-processor)
+ ))
+
+(defun my-wget-dom (url &optional no-tor)
+ (my-wget-out-internal
+ url
+ (lambda () (libxml-parse-html-region (point-min) (point-max)))
+ no-tor))
+
+(defun my-wget-raw (url &optional no-tor)
+ (my-wget-out-internal url 'buffer-string no-tor))
+
(provide 'my-wget)
;;; my-wget.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-ytdl.el b/emacs/.emacs.d/lisp/my/my-ytdl.el
index b3b1cf7..7cdda43 100644
--- a/emacs/.emacs.d/lisp/my/my-ytdl.el
+++ b/emacs/.emacs.d/lisp/my/my-ytdl.el
@@ -26,6 +26,7 @@
;;; Code:
+(require 'tor)
(defvar my-ytdl-program "yt-dlp")
@@ -60,21 +61,34 @@
;; "%(id)s.%(ext)s" ;; for long names
"%(playlist|.)s/%(playlist_index|)s%(playlist_index&-|)s%(title)s.%(ext)s"
"--write-description"
+ "--write-info-json"
"--write-thumbnail"))
(defvar my-ytdl-audio-download-dir "~/Downloads"
"Directory for ytdl to download audios to.")
+(defvar my-ytdl-music-download-dir "~/Downloads"
+ "Directory for ytdl to download music to.")
+
(defun my-ytdl-internal (urls type &optional no-tor)
- (my-with-default-directory (if (eq type 'video)
- my-ytdl-video-download-dir
- my-ytdl-audio-download-dir)
- (apply 'my-start-process-with-torsocks
- (append
- (list no-tor (format "ytdl-%s" urls) (format "*ytdl-%s*" urls)
- my-ytdl-program)
- (if (eq type 'video) my-ytdl-video-args my-ytdl-audio-args)
- (split-string urls)))))
+ (my-with-default-directory (pcase type
+ ('video my-ytdl-video-download-dir)
+ ('audio my-ytdl-audio-download-dir)
+ ('music my-ytdl-music-download-dir)
+ (_ (error "Unsupported type: %s" type)))
+ (set-process-sentinel
+ (apply 'my-start-process-with-torsocks
+ (append
+ (list no-tor (format "ytdl-%s" urls) (format "*ytdl-%s*" urls)
+ my-ytdl-program)
+ (if (eq type 'video) my-ytdl-video-args my-ytdl-audio-args)
+ (split-string urls)))
+ (lambda (proc event)
+ (let ((status (process-exit-status proc)))
+ (if (eq status 0)
+ (progn
+ (message "ytdl-%s %s: DONE" type urls))
+ (message "ytdl-%s %s FAILED: %s" type urls event)))))))
(defun my-ytdl-video-info (url)
"Given a video URL, return an alist of its properties."
@@ -91,7 +105,7 @@
(defun my-ytdl-video-url-p (url)
(let ((urlobj (url-generic-parse-url url)))
(or (and (string-match-p
- "^\\(www\\.\\)?\\(youtube\\.com\\|yewtu\\.be\\)"
+ "^\\(www\\.\\|m\\.\\)?\\(youtube\\.com\\|yewtu\\.be\\)"
(url-host urlobj))
(string-match-p "^/watch\\?v=.*" (url-filename urlobj)))
(equal "youtu.be" (url-host urlobj)))))
@@ -148,11 +162,21 @@
(interactive "sURL(s): ")
(my-ytdl-internal urls 'audio))
+(defun my-ytdl-music (urls)
+ "Download music with ytdl."
+ (interactive "sURL(s): ")
+ (my-ytdl-internal urls 'music))
+
(defun my-ytdl-audio-no-tor (urls)
"Download audio with ytdl."
(interactive "sURL(s): ")
(my-ytdl-internal urls 'audio t))
+(defun my-ytdl-music-no-tor (urls)
+ "Download music with ytdl."
+ (interactive "sURL(s): ")
+ (my-ytdl-internal urls 'music t))
+
;;; fixme: autoload
(defun my-ytdl-video-no-tor (urls)
"Download videos with ytdl."
diff --git a/emacs/.emacs.d/lisp/my/tor.el b/emacs/.emacs.d/lisp/my/tor.el
new file mode 100644
index 0000000..9ed7d5f
--- /dev/null
+++ b/emacs/.emacs.d/lisp/my/tor.el
@@ -0,0 +1,57 @@
+;;; tor.el -- tor utilities -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; Package-Requires: ((emacs "30.1"))
+
+;; This file is part of dotted.
+
+;; dotted is free software: you can redistribute it and/or modify it under
+;; the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; dotted is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+;; Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public
+;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; tor utilities.
+
+;;; Code:
+
+
+(defun my-call-process-with-torsocks (no-tor program
+ &optional infile destination display
+ &rest args)
+ (if no-tor
+ (apply 'call-process
+ (append (list program infile destination display) args))
+ (apply 'call-process
+ (append (list "torsocks" infile destination display program) args))))
+
+(defun my-start-process-with-torsocks (no-tor name buffer program
+ &rest program-args)
+ (if no-tor
+ (apply 'start-process (append (list name buffer program) program-args))
+ (apply 'start-process
+ (append (list name buffer "torsocks" program) program-args))))
+
+(defun tor-parse-check-dom (dom)
+ (let ((content (dom-by-class dom "content")))
+ (format "%s\n%s"
+ (string-trim (dom-text (dom-by-tag content 'h1)))
+ (string-trim (dom-texts (car (dom-by-tag content 'p)))))))
+
+(defun tor-check (&optional no-tor)
+ (require 'my-wget)
+ (tor-parse-check-dom (my-wget-dom "https://check.torproject.org/" no-tor)))
+
+(provide 'tor)
+;;; tor.el ends here
diff --git a/emacs/.emacs.d/lisp/nov.el b/emacs/.emacs.d/lisp/nov.el
-Subproject c0d30da504fb0b68d8c28ff61a5e0095acda7f5
+Subproject 3fcd9edd05062a9c43b60d5361de410c4001090
diff --git a/emacs/.emacs.d/lisp/ttrss.el b/emacs/.emacs.d/lisp/ttrss.el
new file mode 160000
+Subproject d9d51f823cbb5bcd45a59e0fb578a13fa8d6003
diff --git a/manual/singlefile-settings.json b/manual/singlefile-settings.json
new file mode 100644
index 0000000..3cdd2a8
--- /dev/null
+++ b/manual/singlefile-settings.json
@@ -0,0 +1,185 @@
+{
+ "profiles": {
+ "__Default_Settings__": {
+ "removeHiddenElements": true,
+ "removeUnusedStyles": true,
+ "removeUnusedFonts": true,
+ "removeFrames": true,
+ "blockScripts": true,
+ "blockVideos": true,
+ "blockAudios": true,
+ "blockFonts": false,
+ "blockStylesheets": false,
+ "blockImages": false,
+ "acceptHeaders": {
+ "document": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "script": "*/*",
+ "audio": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
+ "video": "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
+ "font": "application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8",
+ "stylesheet": "text/css,*/*;q=0.1",
+ "image": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
+ },
+ "saveRawPage": false,
+ "insertMetaCSP": true,
+ "saveToClipboard": false,
+ "addProof": false,
+ "woleetKey": "",
+ "saveToGDrive": false,
+ "saveToDropbox": false,
+ "saveWithWebDAV": false,
+ "webDAVURL": "",
+ "webDAVUser": "",
+ "webDAVPassword": "",
+ "saveToGitHub": false,
+ "githubToken": "",
+ "githubUser": "",
+ "githubRepository": "SingleFile-Archives",
+ "githubBranch": "main",
+ "saveWithCompanion": false,
+ "sharePage": false,
+ "compressHTML": true,
+ "insertTextBody": false,
+ "insertEmbeddedImage": false,
+ "insertEmbeddedScreenshotImage": false,
+ "compressCSS": false,
+ "groupDuplicateStylesheets": false,
+ "moveStylesInHead": false,
+ "loadDeferredImages": true,
+ "loadDeferredImagesMaxIdleTime": 1500,
+ "loadDeferredImagesKeepZoomLevel": false,
+ "loadDeferredImagesDispatchScrollEvent": false,
+ "loadDeferredImagesBeforeFrames": false,
+ "contextMenuEnabled": true,
+ "filenameTemplate": "%if-empty<{page-title}|No title>.{filename-extension}",
+ "filenameMaxLength": "192",
+ "filenameMaxLengthUnit": "bytes",
+ "filenameReplacementCharacter": "_",
+ "replaceEmojisInFilename": false,
+ "saveFilenameTemplateData": false,
+ "shadowEnabled": true,
+ "maxResourceSizeEnabled": true,
+ "maxResourceSize": 10,
+ "networkTimeout": 0,
+ "confirmFilename": false,
+ "filenameConflictAction": "overwrite",
+ "displayInfobar": true,
+ "displayStats": false,
+ "backgroundSave": true,
+ "autoSaveDelay": 1,
+ "autoSaveLoad": false,
+ "autoSaveUnload": false,
+ "autoSaveDiscard": false,
+ "autoSaveRemove": false,
+ "autoSaveLoadOrUnload": true,
+ "autoSaveRepeat": false,
+ "autoSaveRepeatDelay": 10,
+ "autoSaveExternalSave": false,
+ "removeAlternativeFonts": true,
+ "removeAlternativeImages": true,
+ "removeAlternativeMedias": true,
+ "saveCreatedBookmarks": false,
+ "passReferrerOnError": false,
+ "replaceBookmarkURL": true,
+ "allowedBookmarkFolders": [
+ ""
+ ],
+ "ignoredBookmarkFolders": [
+ ""
+ ],
+ "compressContent": false,
+ "createRootDirectory": false,
+ "preventAppendedData": false,
+ "selfExtractingArchive": false,
+ "extractDataFromPage": false,
+ "password": "",
+ "groupDuplicateImages": true,
+ "infobarTemplate": "",
+ "blockMixedContent": true,
+ "saveOriginalURLs": false,
+ "includeInfobar": false,
+ "openInfobar": false,
+ "removeSavedDate": false,
+ "confirmInfobarContent": false,
+ "autoClose": false,
+ "openEditor": false,
+ "openSavedPage": false,
+ "autoOpenEditor": false,
+ "defaultEditorMode": "normal",
+ "applySystemTheme": true,
+ "warnUnsavedPage": true,
+ "displayInfobarInEditor": false,
+ "saveToRestFormApi": false,
+ "saveToRestFormApiUrl": "",
+ "saveToRestFormApiToken": "",
+ "saveToRestFormApiFileFieldName": "",
+ "saveToRestFormApiUrlFieldName": "",
+ "saveToS3": false,
+ "S3Domain": "s3.amazonaws.com",
+ "S3Region": "",
+ "S3Bucket": "",
+ "S3AccessKey": "",
+ "S3SecretKey": "",
+ "loadDeferredImagesBlockCookies": false,
+ "loadDeferredImagesBlockStorage": false,
+ "filenameReplacedCharacters": [
+ "~",
+ "+",
+ "?",
+ "%",
+ "*",
+ ":",
+ "|",
+ "\"",
+ "<",
+ ">",
+ "\\\\",
+ "\u0000-\u001f",
+ ""
+ ],
+ "filenameReplacementCharacters": [
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_",
+ "_"
+ ],
+ "tabMenuEnabled": true,
+ "browserActionMenuEnabled": true,
+ "logsEnabled": true,
+ "progressBarEnabled": true,
+ "maxSizeDuplicateImages": 524288,
+ "forceWebAuthFlow": false,
+ "resolveFragmentIdentifierURLs": false,
+ "userScriptEnabled": false,
+ "saveFavicon": true,
+ "includeBOM": false,
+ "insertMetaNoIndex": false,
+ "insertSingleFileComment": true,
+ "blockAlternativeImages": true,
+ "delayBeforeProcessing": 0,
+ "_migratedTemplateFormat": true,
+ "resolveLinks": true,
+ "infobarPositionAbsolute": false,
+ "infobarPositionTop": "16px",
+ "infobarPositionRight": "16px",
+ "infobarPositionBottom": "",
+ "infobarPositionLeft": ""
+ }
+ },
+ "rules": [
+ {
+ "url": "file:",
+ "profile": "__Default_Settings__",
+ "autoSaveProfile": "__Disabled_Settings__"
+ }
+ ],
+ "maxParallelWorkers": 12,
+ "processInForeground": false
+} \ No newline at end of file
diff --git a/misc/.config/mpv/mpv.conf b/misc/.config/mpv/mpv.conf
index 3b49398..b6b1beb 100644
--- a/misc/.config/mpv/mpv.conf
+++ b/misc/.config/mpv/mpv.conf
@@ -7,6 +7,8 @@ script-opts=ytdl_hook-ytdl_path=/usr/bin/yt-dlp
ytdl-format="bestvideo[height<=?720]+bestaudio/best"
osc=no
stop-screensaver = "yes"
+# play audio with ui
+# profile=pseudo-gui
[emacsconf-talks]
# Positioning
diff --git a/misc/.gdbinit b/misc/.gdbinit
index b06bc7f..00be34f 100644
--- a/misc/.gdbinit
+++ b/misc/.gdbinit
@@ -37,7 +37,12 @@ alias pvtable = print /a (*(void ***))
set unwindonsignal on
set print vtbl on
set print pretty on
+set index-cache enabled on
# rr
alias rf = reverse-finish
+define wlrc
+ watch -l $arg0
+ rc
+end
source ~/.gdbinit_local
diff --git a/misc/bin/merge-tracks.sh b/misc/bin/merge-tracks.sh
new file mode 100755
index 0000000..ddfc36d
--- /dev/null
+++ b/misc/bin/merge-tracks.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Consolidate albums of < 3 tracks to a "medley" album
+
+find . -wholename './.git' -prune -o -type d -links 2 \
+ -exec /bin/bash -c 'a=( "{}"/* ); [[ ${#a[*]} -lt 3 ]]' ';' -print |
+ while IFS=$'\n' read -r dir; do
+ for f in "$dir"/*; do
+ # echo "$f"
+ p1=$(echo "$f" | cut -d/ -f 2)
+ p2=$(basename "$f")
+ if [[ "$p1" == "Various Artists" ]]; then
+ newf="./Various Artists/Medley/$p2"
+ else
+ newf="./Various Artists/Medley/$p1-$p2"
+ fi
+ git mv "$f" "$newf"
+ done
+ done
+
+# May need to do multiple times to remove all empty dirs
+find . -wholename './.git' -prune -o -type d -empty -print |
+ while IFS=$'\n' read -r dir; do
+ echo rmdir "$dir"
+ done
diff --git a/misc/bin/mpvmix.sh b/misc/bin/mpvmix.sh
new file mode 100755
index 0000000..8bc0ac8
--- /dev/null
+++ b/misc/bin/mpvmix.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# Randomly play media segments from a list of files with start and end
+# time line format:
+# 1:00//1:05//~/file.mp4
+
+f="$1"
+n=$(cat "$f" | wc -l)
+while true; do
+ while read -r line; do
+ echo "$line"
+ regex='([0-9:]+)//([0-9:]+)//(.+)'
+ [[ "$line" =~ $regex ]] || continue
+ start=${BASH_REMATCH[1]}
+ end=${BASH_REMATCH[2]}
+ media=${BASH_REMATCH[3]}
+ media="${media/#~/${HOME}}"
+ # echo "start: $start; end: $end; file: $media"
+ mpv --start=$start --end=$end "$media"
+ done <<<$(shuf -n $n "$f")
+ sleep 1
+done
diff --git a/misc/bin/mv-single-pages.sh b/misc/bin/mv-single-pages.sh
new file mode 100755
index 0000000..76c234f
--- /dev/null
+++ b/misc/bin/mv-single-pages.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+grep -l "^ Page saved with SingleFile $" -Z ~/Downloads/*.html | xargs -0 -I {} mv {} "$MY_WEBPAGE_INCOMING_DIR/"
diff --git a/misc/bin/ttrss-fetch.el b/misc/bin/ttrss-fetch.el
new file mode 100755
index 0000000..770ff46
--- /dev/null
+++ b/misc/bin/ttrss-fetch.el
@@ -0,0 +1,10 @@
+#!/bin/emacs --script
+
+(add-to-list 'load-path (locate-user-emacs-file "lisp/ttrss.el"))
+(add-to-list 'load-path (locate-user-emacs-file "lisp/my"))
+(require 'my-ttrss)
+(require 'my-package)
+(my-read-local-config)
+(my-setq-from-local ttrss-address ttrss-user ttrss-password)
+(my-setq-from-local my-ttrss-dir)
+(my-ttrss-fetch)
diff --git a/misc/bin/unzipall.sh b/misc/bin/unzipall.sh
new file mode 100755
index 0000000..2d654f0
--- /dev/null
+++ b/misc/bin/unzipall.sh
@@ -0,0 +1,8 @@
+#/bin/bash
+
+# unzip all zip/7z files with 7z in pwd
+for f in ./*; do
+ ext=${f##*.}
+ if test "$ext" = zip; then 7z e "$f"; fi;
+ if test "$ext" = 7z; then 7z e "$f"; fi;
+done
diff --git a/misc/bin/zipall.sh b/misc/bin/zipall.sh
new file mode 100755
index 0000000..0a244c2
--- /dev/null
+++ b/misc/bin/zipall.sh
@@ -0,0 +1,9 @@
+#/bin/bash
+
+# zip all non-7z and non-zip files with 7z in pwd and delete the original
+for f in ./*; do
+ ext=${f##*.}
+ if test "$ext" = zip; then continue; fi;
+ if test "$ext" = 7z; then continue; fi;
+ 7z a -sdel "$f.7z" "$f"
+done